@wlv-zedd/dsh-chatgpt-web 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +140 -140
- package/cordis.patch.yml +4 -4
- package/lib/cli.js +13269 -11860
- package/lib/plugin.js +3 -50
- package/package.json +89 -88
- package/src/adapters/base.ts +16 -16
- package/src/adapters/chatgpt-web/adapter-error.ts +59 -59
- package/src/adapters/chatgpt-web/browser-helper-main.ts +513 -513
- package/src/adapters/chatgpt-web/browser-helper-prompt-selection.ts +27 -27
- package/src/adapters/chatgpt-web/browser-worker.ts +4944 -4944
- package/src/adapters/chatgpt-web/codex-rollout-environment.ts +628 -628
- package/src/adapters/chatgpt-web/compaction-handoff.ts +533 -533
- package/src/adapters/chatgpt-web/compaction-transaction.ts +142 -142
- package/src/adapters/chatgpt-web/concurrency.ts +6 -6
- package/src/adapters/chatgpt-web/conversation-key.ts +58 -58
- package/src/adapters/chatgpt-web/environment.ts +669 -669
- package/src/adapters/chatgpt-web/index.ts +1544 -1544
- package/src/adapters/chatgpt-web/input-tokens.ts +74 -74
- package/src/adapters/chatgpt-web/launcher-helper-client.ts +695 -695
- package/src/adapters/chatgpt-web/markdown.ts +418 -418
- package/src/adapters/chatgpt-web/mcp-main.ts +25 -25
- package/src/adapters/chatgpt-web/mcp-server.ts +933 -933
- package/src/adapters/chatgpt-web/model.ts +70 -70
- package/src/adapters/chatgpt-web/native-compaction-control.ts +74 -74
- package/src/adapters/chatgpt-web/output-validation.ts +62 -62
- package/src/adapters/chatgpt-web/process-line-writer.ts +46 -46
- package/src/adapters/chatgpt-web/prompt.ts +702 -702
- package/src/adapters/chatgpt-web/retry-policy.ts +73 -73
- package/src/adapters/chatgpt-web/rolling-checkpoint.ts +384 -384
- package/src/adapters/chatgpt-web/thread-environment.ts +238 -238
- package/src/adapters/chatgpt-web/tool-stream-parser.ts +1 -1
- package/src/adapters/chatgpt-web/turn-broker.ts +1481 -1481
- package/src/adapters/chatgpt-web/turn-execution.ts +816 -816
- package/src/adapters/chatgpt-web/turn-progress.ts +292 -292
- package/src/adapters/chatgpt-web/usage.ts +121 -121
- package/src/adapters/image.ts +9 -9
- package/src/bridge.ts +1083 -1083
- package/src/browser-login.ts +521 -521
- package/src/chatgpt-session.ts +240 -240
- package/src/chatgpt-web-models.ts +400 -400
- package/src/cli.ts +568 -568
- package/src/codex-integration-document.ts +824 -824
- package/src/codex-integration-journal.ts +212 -212
- package/src/codex-integration-route.ts +515 -515
- package/src/codex-integration-shared.ts +332 -332
- package/src/codex-integration.ts +529 -529
- package/src/codex-interrupt-hook.ts +158 -158
- package/src/config.ts +616 -616
- package/src/dev-chat/cli.ts +432 -432
- package/src/dev-chat/constants.ts +3 -3
- package/src/dev-chat/driver.ts +655 -655
- package/src/dev-chat/profile.ts +223 -223
- package/src/dev-chat/session.ts +287 -287
- package/src/dev-chat/transport.ts +54 -54
- package/src/doctor.ts +237 -237
- package/src/event-queue.ts +45 -45
- package/src/http-body.ts +30 -30
- package/src/launcher-browser-host.ts +695 -695
- package/src/lib/errors.ts +281 -281
- package/src/lib/token-estimate.ts +42 -42
- package/src/login-helper.cjs +1 -1
- package/src/model-catalog.ts +197 -197
- package/src/native-passthrough.ts +261 -261
- package/src/plugin.ts +191 -191
- package/src/process.ts +45 -45
- package/src/responses/compaction.ts +199 -199
- package/src/responses/parser.ts +633 -633
- package/src/responses/reasoning-envelope.ts +49 -49
- package/src/responses/schema.ts +172 -172
- package/src/responses/state.ts +230 -230
- package/src/server.ts +1111 -1111
- package/src/service.ts +315 -315
- package/src/setup.ts +671 -671
- package/src/stall-timeout.ts +23 -23
- package/src/tunnel-service.ts +160 -160
- package/src/tunnel.ts +417 -417
- package/src/turndown-plugin-gfm.d.ts +5 -5
- package/src/types.ts +307 -307
- package/src/usage/totals.ts +12 -12
- package/src/version.ts +1 -1
package/src/chatgpt-session.ts
CHANGED
|
@@ -1,240 +1,240 @@
|
|
|
1
|
-
import type { Locator, Page } from "playwright-core";
|
|
2
|
-
import type { ChatGptWebAccountCapabilities } from "./chatgpt-web-models";
|
|
3
|
-
|
|
4
|
-
export const CHATGPT_TEMPORARY_CHAT_URL = "https://chatgpt.com/?temporary-chat=true";
|
|
5
|
-
export const CHATGPT_COMPOSER_SELECTOR = [
|
|
6
|
-
'[data-testid="prompt-textarea"]',
|
|
7
|
-
"#prompt-textarea",
|
|
8
|
-
'[contenteditable="true"][data-lexical-editor="true"]',
|
|
9
|
-
].join(", ");
|
|
10
|
-
export const CHATGPT_EFFORT_CONTROL_SELECTOR = [
|
|
11
|
-
'button[aria-haspopup="menu"][data-tone="neutral"]',
|
|
12
|
-
'button[data-testid="model-switcher-dropdown-button"][aria-haspopup="menu"]',
|
|
13
|
-
].join(", ");
|
|
14
|
-
export const CHATGPT_EFFORT_MENU_SELECTOR = [
|
|
15
|
-
'[data-testid="composer-intelligence-picker-content"]:has([role="menuitemradio"], [data-model-reasoning-effort-slider])',
|
|
16
|
-
'[role="menu"]:has([role="menuitemradio"], [data-model-reasoning-effort-slider])',
|
|
17
|
-
'[role="group"]:has([role="menuitemradio"], [data-model-reasoning-effort-slider])',
|
|
18
|
-
].join(", ");
|
|
19
|
-
export const CHATGPT_EFFORT_ITEM_SELECTOR = '[role="menuitemradio"]';
|
|
20
|
-
export const CHATGPT_EFFORT_SLIDER_SELECTOR = '[data-model-reasoning-effort-slider] [role="slider"]';
|
|
21
|
-
export const CHATGPT_EFFORT_SLIDER_MAX_OPTIONS = 5;
|
|
22
|
-
export const CHATGPT_STOP_BUTTON_SELECTOR = '[data-testid="stop-button"]';
|
|
23
|
-
export const CHATGPT_COMPLETION_ACTION_SELECTOR = 'button[data-testid="copy-turn-action-button"]';
|
|
24
|
-
export const CHATGPT_ASSISTANT_TURN_SELECTOR = [
|
|
25
|
-
'[data-testid^="conversation-turn-"][data-turn="assistant"]',
|
|
26
|
-
'[data-testid^="conversation-turn-"][data-message-author-role="assistant"]',
|
|
27
|
-
'[data-testid^="conversation-turn-"]:has([data-message-author-role="assistant"])',
|
|
28
|
-
].join(", ");
|
|
29
|
-
export const CHATGPT_USER_TURN_SELECTOR = [
|
|
30
|
-
'[data-testid^="conversation-turn-"][data-turn="user"]',
|
|
31
|
-
'[data-testid^="conversation-turn-"][data-message-author-role="user"]',
|
|
32
|
-
'[data-testid^="conversation-turn-"]:has([data-message-author-role="user"])',
|
|
33
|
-
].join(", ");
|
|
34
|
-
|
|
35
|
-
export interface ChatGptEffortSliderState {
|
|
36
|
-
min: number;
|
|
37
|
-
max: number;
|
|
38
|
-
value: number;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface ChatGptEffortActivation {
|
|
42
|
-
method: "already-open" | "click" | "pointerdown";
|
|
43
|
-
menu: Locator;
|
|
44
|
-
slider: Locator;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function effortMenuSelectorForId(menuId: string): string {
|
|
48
|
-
return `[id=${JSON.stringify(menuId)}]`;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export async function chatGptEffortMenuForControl(page: Page, control: Locator): Promise<Locator> {
|
|
52
|
-
const menuId = await control.getAttribute("aria-controls").catch(() => null);
|
|
53
|
-
if (menuId) return page.locator(effortMenuSelectorForId(menuId));
|
|
54
|
-
return page.locator(CHATGPT_EFFORT_MENU_SELECTOR).filter({ visible: true }).last();
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async function visibleEffortSurface(
|
|
58
|
-
page: Page,
|
|
59
|
-
control: Locator,
|
|
60
|
-
): Promise<{ menu: Locator; slider: Locator } | undefined> {
|
|
61
|
-
const menu = await chatGptEffortMenuForControl(page, control);
|
|
62
|
-
const slider = page.locator(CHATGPT_EFFORT_SLIDER_SELECTOR).filter({ visible: true }).last();
|
|
63
|
-
if (await menu.isVisible().catch(() => false) || await slider.isVisible().catch(() => false)) {
|
|
64
|
-
return { menu, slider };
|
|
65
|
-
}
|
|
66
|
-
return undefined;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async function waitForEffortSurface(
|
|
70
|
-
page: Page,
|
|
71
|
-
control: Locator,
|
|
72
|
-
timeoutMs: number,
|
|
73
|
-
): Promise<{ menu: Locator; slider: Locator } | undefined> {
|
|
74
|
-
const deadline = Date.now() + timeoutMs;
|
|
75
|
-
do {
|
|
76
|
-
const surface = await visibleEffortSurface(page, control);
|
|
77
|
-
if (surface) return surface;
|
|
78
|
-
if (Date.now() >= deadline) return undefined;
|
|
79
|
-
await new Promise(resolveSleep => setTimeout(resolveSleep, 50));
|
|
80
|
-
} while (true);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async function clearGhostEffortState(page: Page, control: Locator): Promise<void> {
|
|
84
|
-
const expanded = await control.getAttribute("aria-expanded").catch(() => null);
|
|
85
|
-
const state = await control.getAttribute("data-state").catch(() => null);
|
|
86
|
-
if (expanded === "true" || state === "open") {
|
|
87
|
-
await page.keyboard.press("Escape").catch(() => {});
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export async function activateChatGptEffortMenu(
|
|
92
|
-
page: Page,
|
|
93
|
-
control: Locator,
|
|
94
|
-
options: { settleMs?: number } = {},
|
|
95
|
-
): Promise<ChatGptEffortActivation> {
|
|
96
|
-
const openSurface = await visibleEffortSurface(page, control);
|
|
97
|
-
if (openSurface) return { method: "already-open", ...openSurface };
|
|
98
|
-
|
|
99
|
-
const settleMs = options.settleMs ?? 3_000;
|
|
100
|
-
await clearGhostEffortState(page, control);
|
|
101
|
-
await control.click({ force: true, timeout: Math.max(1, settleMs) });
|
|
102
|
-
const clickedSurface = await waitForEffortSurface(page, control, settleMs);
|
|
103
|
-
if (clickedSurface) return { method: "click", ...clickedSurface };
|
|
104
|
-
|
|
105
|
-
await clearGhostEffortState(page, control);
|
|
106
|
-
await control.dispatchEvent("pointerdown", {
|
|
107
|
-
button: 0,
|
|
108
|
-
buttons: 1,
|
|
109
|
-
pointerType: "mouse",
|
|
110
|
-
isPrimary: true,
|
|
111
|
-
});
|
|
112
|
-
const pointerSurface = await waitForEffortSurface(page, control, settleMs);
|
|
113
|
-
if (pointerSurface) return { method: "pointerdown", ...pointerSurface };
|
|
114
|
-
throw new Error(
|
|
115
|
-
"ChatGPT effort control did not expose its owned menu or structural slider after click and primary pointerdown",
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function safeIntegerAttribute(value: string | null): number | undefined {
|
|
120
|
-
if (value === null || !/^-?\d+$/.test(value)) return undefined;
|
|
121
|
-
const parsed = Number(value);
|
|
122
|
-
return Number.isSafeInteger(parsed) ? parsed : undefined;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export function parseChatGptEffortSliderState(
|
|
126
|
-
rawMin: string | null,
|
|
127
|
-
rawMax: string | null,
|
|
128
|
-
rawValue: string | null,
|
|
129
|
-
): ChatGptEffortSliderState | undefined {
|
|
130
|
-
const min = safeIntegerAttribute(rawMin);
|
|
131
|
-
const max = safeIntegerAttribute(rawMax);
|
|
132
|
-
const value = safeIntegerAttribute(rawValue);
|
|
133
|
-
if (min === undefined || max === undefined || value === undefined) return undefined;
|
|
134
|
-
const optionCount = max - min + 1;
|
|
135
|
-
if (optionCount < 1 || optionCount > CHATGPT_EFFORT_SLIDER_MAX_OPTIONS) return undefined;
|
|
136
|
-
if (value < min || value > max) return undefined;
|
|
137
|
-
return { min, max, value };
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async function anyVisible(locator: Locator): Promise<boolean> {
|
|
141
|
-
const count = await locator.count();
|
|
142
|
-
for (let index = 0; index < count; index += 1) {
|
|
143
|
-
if (await locator.nth(index).isVisible().catch(() => false)) return true;
|
|
144
|
-
}
|
|
145
|
-
return false;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export async function assertAuthenticatedChatGptPage(page: Page): Promise<void> {
|
|
149
|
-
const composer = page.locator(
|
|
150
|
-
CHATGPT_COMPOSER_SELECTOR,
|
|
151
|
-
);
|
|
152
|
-
if (!await anyVisible(composer)) {
|
|
153
|
-
throw new Error("ChatGPT authentication could not be verified: no visible composer is present");
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export async function assertTemporaryChatPage(page: Page): Promise<void> {
|
|
158
|
-
const url = new URL(page.url());
|
|
159
|
-
const expected = new URL(CHATGPT_TEMPORARY_CHAT_URL);
|
|
160
|
-
if (url.origin !== expected.origin || url.pathname !== expected.pathname || url.searchParams.get("temporary-chat") !== "true") {
|
|
161
|
-
throw new Error(`ChatGPT left the isolated Temporary Chat surface (${page.url()})`);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export async function detectChatGptAccountCapabilities(
|
|
166
|
-
page: Page,
|
|
167
|
-
options: { selectorTimeoutMs?: number; stableAbsenceMs?: number } = {},
|
|
168
|
-
): Promise<ChatGptWebAccountCapabilities> {
|
|
169
|
-
const composers = page.locator(CHATGPT_COMPOSER_SELECTOR).filter({ visible: true });
|
|
170
|
-
const composer = composers.last();
|
|
171
|
-
const composerForm = composer.locator("xpath=ancestor::form[1]");
|
|
172
|
-
const effortButton = composerForm.locator(CHATGPT_EFFORT_CONTROL_SELECTOR).last();
|
|
173
|
-
const deadline = Date.now() + (options.selectorTimeoutMs ?? 30_000);
|
|
174
|
-
const stableAbsenceMs = options.stableAbsenceMs ?? 3_000;
|
|
175
|
-
let absenceSince: number | undefined;
|
|
176
|
-
let presenceObservations = 0;
|
|
177
|
-
while (true) {
|
|
178
|
-
const effortVisible = await effortButton.isVisible().catch(() => false);
|
|
179
|
-
if (effortVisible) {
|
|
180
|
-
presenceObservations += 1;
|
|
181
|
-
absenceSince = undefined;
|
|
182
|
-
if (presenceObservations >= 2) break;
|
|
183
|
-
await new Promise(resolveSleep => setTimeout(resolveSleep, 100));
|
|
184
|
-
continue;
|
|
185
|
-
}
|
|
186
|
-
presenceObservations = 0;
|
|
187
|
-
const composerReady = await composers.count().then(count => count === 1).catch(() => false);
|
|
188
|
-
const formReady = await composerForm.count().then(count => count === 1).catch(() => false);
|
|
189
|
-
const documentReady = await page.evaluate(() => document.readyState === "complete").catch(() => false);
|
|
190
|
-
if (composerReady && formReady && documentReady) {
|
|
191
|
-
absenceSince ??= Date.now();
|
|
192
|
-
if (Date.now() - absenceSince >= stableAbsenceMs) {
|
|
193
|
-
return { solAvailable: false, proAvailable: false };
|
|
194
|
-
}
|
|
195
|
-
} else {
|
|
196
|
-
absenceSince = undefined;
|
|
197
|
-
}
|
|
198
|
-
if (Date.now() >= deadline) {
|
|
199
|
-
throw new Error("ChatGPT account capability probe did not reach a stable composer state");
|
|
200
|
-
}
|
|
201
|
-
await new Promise(resolveSleep => setTimeout(resolveSleep, 100));
|
|
202
|
-
}
|
|
203
|
-
const menu = page.locator(CHATGPT_EFFORT_MENU_SELECTOR).last();
|
|
204
|
-
const menuVisible = await menu.isVisible().catch(() => false);
|
|
205
|
-
const menuExpanded = await effortButton.getAttribute("aria-expanded").catch(() => null);
|
|
206
|
-
if (!menuVisible && menuExpanded !== "true") await effortButton.press("Enter");
|
|
207
|
-
try {
|
|
208
|
-
const efforts = menu.locator(CHATGPT_EFFORT_ITEM_SELECTOR);
|
|
209
|
-
const slider = page.locator(CHATGPT_EFFORT_SLIDER_SELECTOR).filter({ visible: true }).last();
|
|
210
|
-
const waitAbort = new AbortController();
|
|
211
|
-
try {
|
|
212
|
-
const ready = await Promise.race([
|
|
213
|
-
efforts.first().waitFor({ state: "visible", timeout: 70_000, signal: waitAbort.signal })
|
|
214
|
-
.then(() => "items" as const),
|
|
215
|
-
slider.waitFor({ state: "visible", timeout: 70_000, signal: waitAbort.signal })
|
|
216
|
-
.then(() => "slider" as const),
|
|
217
|
-
]);
|
|
218
|
-
const sliderVisible = ready === "slider" || await slider.isVisible().catch(() => false);
|
|
219
|
-
if (!sliderVisible) {
|
|
220
|
-
return { solAvailable: true, proAvailable: await efforts.count() >= 5 };
|
|
221
|
-
}
|
|
222
|
-
const state = parseChatGptEffortSliderState(
|
|
223
|
-
await slider.getAttribute("aria-valuemin"),
|
|
224
|
-
await slider.getAttribute("aria-valuemax"),
|
|
225
|
-
await slider.getAttribute("aria-valuenow"),
|
|
226
|
-
);
|
|
227
|
-
if (!state) {
|
|
228
|
-
throw new Error(
|
|
229
|
-
"ChatGPT model controls are unavailable. Reload ChatGPT and run Repair again.",
|
|
230
|
-
{ cause: new Error("ChatGPT effort slider exposed an invalid ARIA range") },
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
return { solAvailable: true, proAvailable: state.max - state.min + 1 >= 5 };
|
|
234
|
-
} finally {
|
|
235
|
-
waitAbort.abort();
|
|
236
|
-
}
|
|
237
|
-
} finally {
|
|
238
|
-
await page.keyboard.press("Escape").catch(() => {});
|
|
239
|
-
}
|
|
240
|
-
}
|
|
1
|
+
import type { Locator, Page } from "playwright-core";
|
|
2
|
+
import type { ChatGptWebAccountCapabilities } from "./chatgpt-web-models";
|
|
3
|
+
|
|
4
|
+
export const CHATGPT_TEMPORARY_CHAT_URL = "https://chatgpt.com/?temporary-chat=true";
|
|
5
|
+
export const CHATGPT_COMPOSER_SELECTOR = [
|
|
6
|
+
'[data-testid="prompt-textarea"]',
|
|
7
|
+
"#prompt-textarea",
|
|
8
|
+
'[contenteditable="true"][data-lexical-editor="true"]',
|
|
9
|
+
].join(", ");
|
|
10
|
+
export const CHATGPT_EFFORT_CONTROL_SELECTOR = [
|
|
11
|
+
'button[aria-haspopup="menu"][data-tone="neutral"]',
|
|
12
|
+
'button[data-testid="model-switcher-dropdown-button"][aria-haspopup="menu"]',
|
|
13
|
+
].join(", ");
|
|
14
|
+
export const CHATGPT_EFFORT_MENU_SELECTOR = [
|
|
15
|
+
'[data-testid="composer-intelligence-picker-content"]:has([role="menuitemradio"], [data-model-reasoning-effort-slider])',
|
|
16
|
+
'[role="menu"]:has([role="menuitemradio"], [data-model-reasoning-effort-slider])',
|
|
17
|
+
'[role="group"]:has([role="menuitemradio"], [data-model-reasoning-effort-slider])',
|
|
18
|
+
].join(", ");
|
|
19
|
+
export const CHATGPT_EFFORT_ITEM_SELECTOR = '[role="menuitemradio"]';
|
|
20
|
+
export const CHATGPT_EFFORT_SLIDER_SELECTOR = '[data-model-reasoning-effort-slider] [role="slider"]';
|
|
21
|
+
export const CHATGPT_EFFORT_SLIDER_MAX_OPTIONS = 5;
|
|
22
|
+
export const CHATGPT_STOP_BUTTON_SELECTOR = '[data-testid="stop-button"]';
|
|
23
|
+
export const CHATGPT_COMPLETION_ACTION_SELECTOR = 'button[data-testid="copy-turn-action-button"]';
|
|
24
|
+
export const CHATGPT_ASSISTANT_TURN_SELECTOR = [
|
|
25
|
+
'[data-testid^="conversation-turn-"][data-turn="assistant"]',
|
|
26
|
+
'[data-testid^="conversation-turn-"][data-message-author-role="assistant"]',
|
|
27
|
+
'[data-testid^="conversation-turn-"]:has([data-message-author-role="assistant"])',
|
|
28
|
+
].join(", ");
|
|
29
|
+
export const CHATGPT_USER_TURN_SELECTOR = [
|
|
30
|
+
'[data-testid^="conversation-turn-"][data-turn="user"]',
|
|
31
|
+
'[data-testid^="conversation-turn-"][data-message-author-role="user"]',
|
|
32
|
+
'[data-testid^="conversation-turn-"]:has([data-message-author-role="user"])',
|
|
33
|
+
].join(", ");
|
|
34
|
+
|
|
35
|
+
export interface ChatGptEffortSliderState {
|
|
36
|
+
min: number;
|
|
37
|
+
max: number;
|
|
38
|
+
value: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ChatGptEffortActivation {
|
|
42
|
+
method: "already-open" | "click" | "pointerdown";
|
|
43
|
+
menu: Locator;
|
|
44
|
+
slider: Locator;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function effortMenuSelectorForId(menuId: string): string {
|
|
48
|
+
return `[id=${JSON.stringify(menuId)}]`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function chatGptEffortMenuForControl(page: Page, control: Locator): Promise<Locator> {
|
|
52
|
+
const menuId = await control.getAttribute("aria-controls").catch(() => null);
|
|
53
|
+
if (menuId) return page.locator(effortMenuSelectorForId(menuId));
|
|
54
|
+
return page.locator(CHATGPT_EFFORT_MENU_SELECTOR).filter({ visible: true }).last();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function visibleEffortSurface(
|
|
58
|
+
page: Page,
|
|
59
|
+
control: Locator,
|
|
60
|
+
): Promise<{ menu: Locator; slider: Locator } | undefined> {
|
|
61
|
+
const menu = await chatGptEffortMenuForControl(page, control);
|
|
62
|
+
const slider = page.locator(CHATGPT_EFFORT_SLIDER_SELECTOR).filter({ visible: true }).last();
|
|
63
|
+
if (await menu.isVisible().catch(() => false) || await slider.isVisible().catch(() => false)) {
|
|
64
|
+
return { menu, slider };
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function waitForEffortSurface(
|
|
70
|
+
page: Page,
|
|
71
|
+
control: Locator,
|
|
72
|
+
timeoutMs: number,
|
|
73
|
+
): Promise<{ menu: Locator; slider: Locator } | undefined> {
|
|
74
|
+
const deadline = Date.now() + timeoutMs;
|
|
75
|
+
do {
|
|
76
|
+
const surface = await visibleEffortSurface(page, control);
|
|
77
|
+
if (surface) return surface;
|
|
78
|
+
if (Date.now() >= deadline) return undefined;
|
|
79
|
+
await new Promise(resolveSleep => setTimeout(resolveSleep, 50));
|
|
80
|
+
} while (true);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function clearGhostEffortState(page: Page, control: Locator): Promise<void> {
|
|
84
|
+
const expanded = await control.getAttribute("aria-expanded").catch(() => null);
|
|
85
|
+
const state = await control.getAttribute("data-state").catch(() => null);
|
|
86
|
+
if (expanded === "true" || state === "open") {
|
|
87
|
+
await page.keyboard.press("Escape").catch(() => {});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function activateChatGptEffortMenu(
|
|
92
|
+
page: Page,
|
|
93
|
+
control: Locator,
|
|
94
|
+
options: { settleMs?: number } = {},
|
|
95
|
+
): Promise<ChatGptEffortActivation> {
|
|
96
|
+
const openSurface = await visibleEffortSurface(page, control);
|
|
97
|
+
if (openSurface) return { method: "already-open", ...openSurface };
|
|
98
|
+
|
|
99
|
+
const settleMs = options.settleMs ?? 3_000;
|
|
100
|
+
await clearGhostEffortState(page, control);
|
|
101
|
+
await control.click({ force: true, timeout: Math.max(1, settleMs) });
|
|
102
|
+
const clickedSurface = await waitForEffortSurface(page, control, settleMs);
|
|
103
|
+
if (clickedSurface) return { method: "click", ...clickedSurface };
|
|
104
|
+
|
|
105
|
+
await clearGhostEffortState(page, control);
|
|
106
|
+
await control.dispatchEvent("pointerdown", {
|
|
107
|
+
button: 0,
|
|
108
|
+
buttons: 1,
|
|
109
|
+
pointerType: "mouse",
|
|
110
|
+
isPrimary: true,
|
|
111
|
+
});
|
|
112
|
+
const pointerSurface = await waitForEffortSurface(page, control, settleMs);
|
|
113
|
+
if (pointerSurface) return { method: "pointerdown", ...pointerSurface };
|
|
114
|
+
throw new Error(
|
|
115
|
+
"ChatGPT effort control did not expose its owned menu or structural slider after click and primary pointerdown",
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function safeIntegerAttribute(value: string | null): number | undefined {
|
|
120
|
+
if (value === null || !/^-?\d+$/.test(value)) return undefined;
|
|
121
|
+
const parsed = Number(value);
|
|
122
|
+
return Number.isSafeInteger(parsed) ? parsed : undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function parseChatGptEffortSliderState(
|
|
126
|
+
rawMin: string | null,
|
|
127
|
+
rawMax: string | null,
|
|
128
|
+
rawValue: string | null,
|
|
129
|
+
): ChatGptEffortSliderState | undefined {
|
|
130
|
+
const min = safeIntegerAttribute(rawMin);
|
|
131
|
+
const max = safeIntegerAttribute(rawMax);
|
|
132
|
+
const value = safeIntegerAttribute(rawValue);
|
|
133
|
+
if (min === undefined || max === undefined || value === undefined) return undefined;
|
|
134
|
+
const optionCount = max - min + 1;
|
|
135
|
+
if (optionCount < 1 || optionCount > CHATGPT_EFFORT_SLIDER_MAX_OPTIONS) return undefined;
|
|
136
|
+
if (value < min || value > max) return undefined;
|
|
137
|
+
return { min, max, value };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function anyVisible(locator: Locator): Promise<boolean> {
|
|
141
|
+
const count = await locator.count();
|
|
142
|
+
for (let index = 0; index < count; index += 1) {
|
|
143
|
+
if (await locator.nth(index).isVisible().catch(() => false)) return true;
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function assertAuthenticatedChatGptPage(page: Page): Promise<void> {
|
|
149
|
+
const composer = page.locator(
|
|
150
|
+
CHATGPT_COMPOSER_SELECTOR,
|
|
151
|
+
);
|
|
152
|
+
if (!await anyVisible(composer)) {
|
|
153
|
+
throw new Error("ChatGPT authentication could not be verified: no visible composer is present");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function assertTemporaryChatPage(page: Page): Promise<void> {
|
|
158
|
+
const url = new URL(page.url());
|
|
159
|
+
const expected = new URL(CHATGPT_TEMPORARY_CHAT_URL);
|
|
160
|
+
if (url.origin !== expected.origin || url.pathname !== expected.pathname || url.searchParams.get("temporary-chat") !== "true") {
|
|
161
|
+
throw new Error(`ChatGPT left the isolated Temporary Chat surface (${page.url()})`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function detectChatGptAccountCapabilities(
|
|
166
|
+
page: Page,
|
|
167
|
+
options: { selectorTimeoutMs?: number; stableAbsenceMs?: number } = {},
|
|
168
|
+
): Promise<ChatGptWebAccountCapabilities> {
|
|
169
|
+
const composers = page.locator(CHATGPT_COMPOSER_SELECTOR).filter({ visible: true });
|
|
170
|
+
const composer = composers.last();
|
|
171
|
+
const composerForm = composer.locator("xpath=ancestor::form[1]");
|
|
172
|
+
const effortButton = composerForm.locator(CHATGPT_EFFORT_CONTROL_SELECTOR).last();
|
|
173
|
+
const deadline = Date.now() + (options.selectorTimeoutMs ?? 30_000);
|
|
174
|
+
const stableAbsenceMs = options.stableAbsenceMs ?? 3_000;
|
|
175
|
+
let absenceSince: number | undefined;
|
|
176
|
+
let presenceObservations = 0;
|
|
177
|
+
while (true) {
|
|
178
|
+
const effortVisible = await effortButton.isVisible().catch(() => false);
|
|
179
|
+
if (effortVisible) {
|
|
180
|
+
presenceObservations += 1;
|
|
181
|
+
absenceSince = undefined;
|
|
182
|
+
if (presenceObservations >= 2) break;
|
|
183
|
+
await new Promise(resolveSleep => setTimeout(resolveSleep, 100));
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
presenceObservations = 0;
|
|
187
|
+
const composerReady = await composers.count().then(count => count === 1).catch(() => false);
|
|
188
|
+
const formReady = await composerForm.count().then(count => count === 1).catch(() => false);
|
|
189
|
+
const documentReady = await page.evaluate(() => document.readyState === "complete").catch(() => false);
|
|
190
|
+
if (composerReady && formReady && documentReady) {
|
|
191
|
+
absenceSince ??= Date.now();
|
|
192
|
+
if (Date.now() - absenceSince >= stableAbsenceMs) {
|
|
193
|
+
return { solAvailable: false, proAvailable: false };
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
absenceSince = undefined;
|
|
197
|
+
}
|
|
198
|
+
if (Date.now() >= deadline) {
|
|
199
|
+
throw new Error("ChatGPT account capability probe did not reach a stable composer state");
|
|
200
|
+
}
|
|
201
|
+
await new Promise(resolveSleep => setTimeout(resolveSleep, 100));
|
|
202
|
+
}
|
|
203
|
+
const menu = page.locator(CHATGPT_EFFORT_MENU_SELECTOR).last();
|
|
204
|
+
const menuVisible = await menu.isVisible().catch(() => false);
|
|
205
|
+
const menuExpanded = await effortButton.getAttribute("aria-expanded").catch(() => null);
|
|
206
|
+
if (!menuVisible && menuExpanded !== "true") await effortButton.press("Enter");
|
|
207
|
+
try {
|
|
208
|
+
const efforts = menu.locator(CHATGPT_EFFORT_ITEM_SELECTOR);
|
|
209
|
+
const slider = page.locator(CHATGPT_EFFORT_SLIDER_SELECTOR).filter({ visible: true }).last();
|
|
210
|
+
const waitAbort = new AbortController();
|
|
211
|
+
try {
|
|
212
|
+
const ready = await Promise.race([
|
|
213
|
+
efforts.first().waitFor({ state: "visible", timeout: 70_000, signal: waitAbort.signal })
|
|
214
|
+
.then(() => "items" as const),
|
|
215
|
+
slider.waitFor({ state: "visible", timeout: 70_000, signal: waitAbort.signal })
|
|
216
|
+
.then(() => "slider" as const),
|
|
217
|
+
]);
|
|
218
|
+
const sliderVisible = ready === "slider" || await slider.isVisible().catch(() => false);
|
|
219
|
+
if (!sliderVisible) {
|
|
220
|
+
return { solAvailable: true, proAvailable: await efforts.count() >= 5 };
|
|
221
|
+
}
|
|
222
|
+
const state = parseChatGptEffortSliderState(
|
|
223
|
+
await slider.getAttribute("aria-valuemin"),
|
|
224
|
+
await slider.getAttribute("aria-valuemax"),
|
|
225
|
+
await slider.getAttribute("aria-valuenow"),
|
|
226
|
+
);
|
|
227
|
+
if (!state) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
"ChatGPT model controls are unavailable. Reload ChatGPT and run Repair again.",
|
|
230
|
+
{ cause: new Error("ChatGPT effort slider exposed an invalid ARIA range") },
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
return { solAvailable: true, proAvailable: state.max - state.min + 1 >= 5 };
|
|
234
|
+
} finally {
|
|
235
|
+
waitAbort.abort();
|
|
236
|
+
}
|
|
237
|
+
} finally {
|
|
238
|
+
await page.keyboard.press("Escape").catch(() => {});
|
|
239
|
+
}
|
|
240
|
+
}
|