@steipete/oracle 0.17.3 → 0.19.0
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/README.md +1 -1
- package/dist/bin/oracle-cli.js +75 -64
- package/dist/bin/oracle-mcp.js +0 -0
- package/dist/src/browser/actions/archiveConversation.js +31 -7
- package/dist/src/browser/actions/assistantResponse.js +43 -1
- package/dist/src/browser/actions/attachmentContext.js +239 -0
- package/dist/src/browser/actions/attachmentDataTransfer.js +19 -1
- package/dist/src/browser/actions/attachmentEvidence.js +78 -0
- package/dist/src/browser/actions/attachmentProgress.js +41 -0
- package/dist/src/browser/actions/attachmentPrompt.js +130 -0
- package/dist/src/browser/actions/attachments.js +306 -355
- package/dist/src/browser/actions/deepResearch.js +2 -1
- package/dist/src/browser/actions/modelSelection.js +13 -3
- package/dist/src/browser/actions/navigation.js +1 -1
- package/dist/src/browser/actions/promptComposer.js +361 -186
- package/dist/src/browser/actions/remoteFileTransfer.js +4 -2
- package/dist/src/browser/actions/thinkingStatus.js +2 -5
- package/dist/src/browser/actions/thinkingTime.js +267 -15
- package/dist/src/browser/attachRunning.js +46 -2
- package/dist/src/browser/attachmentValidation.js +53 -0
- package/dist/src/browser/cancellation.js +105 -0
- package/dist/src/browser/chatgptImages.js +2 -0
- package/dist/src/browser/chatgptThrottle.js +104 -0
- package/dist/src/browser/chromeLifecycle.js +267 -43
- package/dist/src/browser/config.js +15 -3
- package/dist/src/browser/detect.js +1 -1
- package/dist/src/browser/index.js +447 -263
- package/dist/src/browser/modelDisplay.js +9 -0
- package/dist/src/browser/pageActions.js +2 -2
- package/dist/src/browser/policies.js +6 -2
- package/dist/src/browser/profileState.js +114 -9
- package/dist/src/browser/projectSourcesRunner.js +88 -17
- package/dist/src/browser/prompt.js +272 -113
- package/dist/src/browser/providers/chatgptDomProvider.js +2 -0
- package/dist/src/browser/reattach.js +14 -4
- package/dist/src/browser/sessionRunner.js +71 -11
- package/dist/src/browser/tabLeaseRegistry.js +414 -38
- package/dist/src/browser/utils.js +20 -2
- package/dist/src/cli/browserConfig.js +40 -15
- package/dist/src/cli/browserDefaults.js +13 -0
- package/dist/src/cli/browserTabs.js +12 -29
- package/dist/src/cli/detachedSession.js +69 -0
- package/dist/src/cli/dryRun.js +58 -47
- package/dist/src/cli/harvestIntegrity.js +140 -0
- package/dist/src/cli/projectSources.js +5 -3
- package/dist/src/cli/serveBrowserConfig.js +24 -0
- package/dist/src/cli/sessionDisplay.js +9 -2
- package/dist/src/cli/sessionRunner.js +91 -7
- package/dist/src/config.js +1 -0
- package/dist/src/gemini-web/client.js +67 -21
- package/dist/src/gemini-web/executor.js +4 -0
- package/dist/src/gemini-web/http.js +15 -0
- package/dist/src/gemini-web/models.js +15 -11
- package/dist/src/mcp/server.js +11 -14
- package/dist/src/mcp/tools/chatgptImage.js +4 -4
- package/dist/src/mcp/tools/consult.js +97 -26
- package/dist/src/mcp/tools/projectSources.js +4 -6
- package/dist/src/mcp/tools/sessionResources.js +1 -1
- package/dist/src/mcp/tools/sessions.js +2 -2
- package/dist/src/mcp/tools/wait.js +182 -0
- package/dist/src/mcp/types.js +7 -0
- package/dist/src/oracle/errors.js +11 -0
- package/dist/src/oracle/providerRoutePlan.js +3 -2
- package/dist/src/remote/client.js +70 -6
- package/dist/src/remote/health.js +6 -2
- package/dist/src/remote/runSlots.js +83 -0
- package/dist/src/remote/server.js +351 -72
- package/dist/src/sessionManager.js +36 -1
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/package.json +25 -23
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/bin/oracle.js +0 -683
- package/dist/docs-site/.nojekyll +0 -0
- package/dist/docs-site/CNAME +0 -1
- package/dist/docs-site/RELEASING.html +0 -410
- package/dist/docs-site/agents.html +0 -374
- package/dist/docs-site/anthropic.html +0 -368
- package/dist/docs-site/bridge.html +0 -400
- package/dist/docs-site/browser-mode.html +0 -588
- package/dist/docs-site/chromium-forks.html +0 -347
- package/dist/docs-site/cli-reference.html +0 -346
- package/dist/docs-site/configuration.html +0 -436
- package/dist/docs-site/favicon.svg +0 -14
- package/dist/docs-site/followup.html +0 -368
- package/dist/docs-site/gemini.html +0 -379
- package/dist/docs-site/grok.html +0 -325
- package/dist/docs-site/index.html +0 -360
- package/dist/docs-site/install.html +0 -335
- package/dist/docs-site/linux.html +0 -321
- package/dist/docs-site/llms.txt +0 -43
- package/dist/docs-site/manual-tests.html +0 -595
- package/dist/docs-site/mcp.html +0 -368
- package/dist/docs-site/multimodel.html +0 -364
- package/dist/docs-site/mythical-pro-agents.html +0 -360
- package/dist/docs-site/notifier.html +0 -338
- package/dist/docs-site/openai-endpoints.html +0 -387
- package/dist/docs-site/openrouter.html +0 -344
- package/dist/docs-site/quickstart.html +0 -369
- package/dist/docs-site/refactor/ux.html +0 -532
- package/dist/docs-site/sessions.html +0 -388
- package/dist/docs-site/social-card.png +0 -0
- package/dist/docs-site/social-card.svg +0 -79
- package/dist/docs-site/spec.html +0 -363
- package/dist/docs-site/testing.html +0 -320
- package/dist/docs-site/tui-debug.html +0 -326
- package/dist/docs-site/windows-work.html +0 -323
- package/dist/docs-site/windows.html +0 -320
- package/dist/markdansi/types/index.js +0 -4
- package/dist/oracle/bin/oracle-cli.js +0 -472
- package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
- package/dist/oracle/src/browser/actions/attachments.js +0 -82
- package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
- package/dist/oracle/src/browser/actions/navigation.js +0 -75
- package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
- package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
- package/dist/oracle/src/browser/config.js +0 -33
- package/dist/oracle/src/browser/constants.js +0 -40
- package/dist/oracle/src/browser/cookies.js +0 -210
- package/dist/oracle/src/browser/domDebug.js +0 -36
- package/dist/oracle/src/browser/index.js +0 -331
- package/dist/oracle/src/browser/pageActions.js +0 -5
- package/dist/oracle/src/browser/prompt.js +0 -88
- package/dist/oracle/src/browser/promptSummary.js +0 -20
- package/dist/oracle/src/browser/sessionRunner.js +0 -80
- package/dist/oracle/src/browser/types.js +0 -1
- package/dist/oracle/src/browser/utils.js +0 -62
- package/dist/oracle/src/browserMode.js +0 -1
- package/dist/oracle/src/cli/browserConfig.js +0 -44
- package/dist/oracle/src/cli/dryRun.js +0 -59
- package/dist/oracle/src/cli/engine.js +0 -17
- package/dist/oracle/src/cli/errorUtils.js +0 -9
- package/dist/oracle/src/cli/help.js +0 -70
- package/dist/oracle/src/cli/markdownRenderer.js +0 -15
- package/dist/oracle/src/cli/options.js +0 -103
- package/dist/oracle/src/cli/promptRequirement.js +0 -14
- package/dist/oracle/src/cli/rootAlias.js +0 -30
- package/dist/oracle/src/cli/sessionCommand.js +0 -77
- package/dist/oracle/src/cli/sessionDisplay.js +0 -270
- package/dist/oracle/src/cli/sessionRunner.js +0 -94
- package/dist/oracle/src/heartbeat.js +0 -43
- package/dist/oracle/src/oracle/client.js +0 -48
- package/dist/oracle/src/oracle/config.js +0 -29
- package/dist/oracle/src/oracle/errors.js +0 -101
- package/dist/oracle/src/oracle/files.js +0 -220
- package/dist/oracle/src/oracle/format.js +0 -33
- package/dist/oracle/src/oracle/fsAdapter.js +0 -7
- package/dist/oracle/src/oracle/oscProgress.js +0 -60
- package/dist/oracle/src/oracle/request.js +0 -48
- package/dist/oracle/src/oracle/run.js +0 -444
- package/dist/oracle/src/oracle/tokenStats.js +0 -39
- package/dist/oracle/src/oracle/types.js +0 -1
- package/dist/oracle/src/oracle.js +0 -9
- package/dist/oracle/src/sessionManager.js +0 -205
- package/dist/oracle/src/version.js +0 -39
- package/dist/scripts/chrome/browser-tools.js +0 -295
- package/dist/src/browser/chromeCookies.js +0 -312
- package/dist/src/browser/keytarShim.js +0 -56
- package/dist/src/browser/profileSync.js +0 -141
- package/dist/src/browser/windowsCookies.js +0 -219
|
@@ -1,9 +1,174 @@
|
|
|
1
|
+
import { withoutBrowserCancellation } from "../cancellation.js";
|
|
1
2
|
import path from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
2
4
|
import { INPUT_SELECTORS, SEND_BUTTON_SELECTORS, UPLOAD_STATUS_SELECTORS } from "../constants.js";
|
|
3
5
|
import { buildConversationTurnListExpression } from "../conversationTurns.js";
|
|
4
6
|
import { delay } from "../utils.js";
|
|
5
7
|
import { logDomFailure } from "../domDebug.js";
|
|
8
|
+
import { BrowserAutomationError } from "../../oracle/errors.js";
|
|
6
9
|
import { transferAttachmentViaDataTransfer } from "./attachmentDataTransfer.js";
|
|
10
|
+
import { buildAttachmentProgressExpression } from "./attachmentProgress.js";
|
|
11
|
+
import { assertComposerPlusStayedInPlace, assertComposerNavigationSnapshot, buildComposerNavigationValidationExpression, buildComposerNavigationProbeExpression, withGuardedFileInput, dispatchGuardedFileInputEvents, clearGuardedFileInput, } from "./attachmentContext.js";
|
|
12
|
+
export { captureComposerNavigationUrl, assertComposerPlusStayedInPlace, buildComposerNavigationProbeExpression, assertComposerNavigationSnapshot, composerNavigationIdentityFromUrl, } from "./attachmentContext.js";
|
|
13
|
+
import { beginAttachmentEvidence, confirmAttachmentEvidence, buildAttachmentEvidenceExpression, } from "./attachmentEvidence.js";
|
|
14
|
+
export function buildAttachmentNamePattern(expectedName, allowStemOnly = false) {
|
|
15
|
+
const baseName = expectedName.split("/").pop()?.split("\\").pop() ?? expectedName;
|
|
16
|
+
const normalizedExpected = baseName.toLowerCase().replace(/\s+/g, " ").trim();
|
|
17
|
+
if (!normalizedExpected)
|
|
18
|
+
return null;
|
|
19
|
+
const escape = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
20
|
+
// Unicode letters, numbers, and combining marks continue a filename, not its label.
|
|
21
|
+
const filenameChars = String.raw `\p{L}\p{N}\p{M}._-`;
|
|
22
|
+
const boundary = `(?=$|[^${filenameChars}])`;
|
|
23
|
+
const extensionIndex = normalizedExpected.lastIndexOf(".");
|
|
24
|
+
let name = `${escape(normalizedExpected)}${boundary}`;
|
|
25
|
+
if (extensionIndex > 0 && extensionIndex < normalizedExpected.length - 1) {
|
|
26
|
+
const stem = normalizedExpected.slice(0, extensionIndex);
|
|
27
|
+
const extension = escape(normalizedExpected.slice(extensionIndex));
|
|
28
|
+
name = `${escape(stem)}(?:\\s*\\([0-9]+(?:-[0-9]+)*\\))?${extension}${boundary}`;
|
|
29
|
+
if (allowStemOnly && stem.length >= 6) {
|
|
30
|
+
// A visible extension or collision suffix must not fall through to a bare stem.
|
|
31
|
+
name = `(?:${name}|${escape(stem)}(?!\\s*[.(])${boundary})`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return new RegExp(`(?:^|[^${filenameChars}])${name}`, "iu");
|
|
35
|
+
}
|
|
36
|
+
export async function activateComposerPlus(runtime, input, navigationUrl) {
|
|
37
|
+
const guardId = randomUUID();
|
|
38
|
+
const probe = await Promise.resolve(runtime.evaluate({
|
|
39
|
+
expression: `(() => {
|
|
40
|
+
const navigation = ${navigationUrl ? buildComposerNavigationValidationExpression(navigationUrl) : buildComposerNavigationProbeExpression()};
|
|
41
|
+
const startUrl = navigation.currentUrl;
|
|
42
|
+
if (navigation.contextMatches === false) return { status: 'context-changed', startUrl, navigation };
|
|
43
|
+
if (navigation.workSelected) {
|
|
44
|
+
return { status: 'work-selected', startUrl };
|
|
45
|
+
}
|
|
46
|
+
if (navigation.modeUnverified) return { status: 'mode-unverified', startUrl };
|
|
47
|
+
const selectors = ['#composer-plus-btn', 'button[data-testid="composer-plus-btn"]'];
|
|
48
|
+
for (const selector of selectors) {
|
|
49
|
+
const node = document.querySelector(selector);
|
|
50
|
+
if (!(node instanceof HTMLElement)) continue;
|
|
51
|
+
const rect = node.getBoundingClientRect();
|
|
52
|
+
if (rect.width <= 0 || rect.height <= 0) continue;
|
|
53
|
+
const nodes = window.__oracleAttachmentPlusNodes ??= Object.create(null);
|
|
54
|
+
nodes[${JSON.stringify(guardId)}] = node;
|
|
55
|
+
node.focus({ preventScroll: true });
|
|
56
|
+
return { status: 'focused', startUrl, focused: document.activeElement === node };
|
|
57
|
+
}
|
|
58
|
+
return { status: 'missing', startUrl };
|
|
59
|
+
})()`,
|
|
60
|
+
returnByValue: true,
|
|
61
|
+
})).then((result) => result?.result?.value);
|
|
62
|
+
const startUrl = navigationUrl ?? (typeof probe?.startUrl === "string" ? probe.startUrl : "");
|
|
63
|
+
if (probe?.status === "context-changed") {
|
|
64
|
+
assertComposerNavigationSnapshot(startUrl, probe.navigation);
|
|
65
|
+
}
|
|
66
|
+
if (probe?.status === "mode-unverified") {
|
|
67
|
+
throw new BrowserAutomationError("ChatGPT mode could not be verified; attachment preparation was stopped.", {
|
|
68
|
+
stage: "upload-attachment",
|
|
69
|
+
code: "attachment-control-mode-unverified",
|
|
70
|
+
startUrl,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
if (probe?.status === "work-selected") {
|
|
74
|
+
throw new BrowserAutomationError("Oracle refused to open the attachment menu because ChatGPT is in Work mode.", {
|
|
75
|
+
stage: "upload-attachment",
|
|
76
|
+
code: "attachment-control-work-mode",
|
|
77
|
+
startUrl,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
if (probe?.status !== "focused") {
|
|
81
|
+
return { method: "unavailable", startUrl };
|
|
82
|
+
}
|
|
83
|
+
const trusted = Boolean(input && typeof input.dispatchKeyEvent === "function");
|
|
84
|
+
let delivery;
|
|
85
|
+
try {
|
|
86
|
+
const prepared = await runtime.evaluate({
|
|
87
|
+
expression: `(() => {
|
|
88
|
+
const button = window.__oracleAttachmentPlusNodes?.[${JSON.stringify(guardId)}];
|
|
89
|
+
const check = () => {
|
|
90
|
+
const navigation = ${buildComposerNavigationValidationExpression(startUrl)};
|
|
91
|
+
const rect = button?.getBoundingClientRect();
|
|
92
|
+
return { ...navigation, focused: button instanceof HTMLElement && button.isConnected &&
|
|
93
|
+
document.activeElement === button && document.querySelector('#composer-plus-btn, button[data-testid="composer-plus-btn"]') === button &&
|
|
94
|
+
!button.hasAttribute('disabled') && button.getAttribute('aria-disabled') !== 'true' &&
|
|
95
|
+
rect.width > 0 && rect.height > 0 };
|
|
96
|
+
};
|
|
97
|
+
const safeCheck = () => { try { return check(); } catch { return { currentUrl: location.href, contextMatches: false, focused: false }; } };
|
|
98
|
+
const guard = { sawKeyDown: false, clicked: false, blocked: null };
|
|
99
|
+
const cancel = (event, state) => { event.preventDefault(); event.stopImmediatePropagation(); guard.blocked = state; };
|
|
100
|
+
const onKey = event => {
|
|
101
|
+
if (event.key !== 'Enter' || !event.isTrusted) return;
|
|
102
|
+
if (event.type === 'keyup') { if (guard.blocked) cancel(event, guard.blocked); return; }
|
|
103
|
+
guard.sawKeyDown = true;
|
|
104
|
+
const state = safeCheck();
|
|
105
|
+
if (guard.blocked || !state.contextMatches || !state.focused) cancel(event, guard.blocked ?? state);
|
|
106
|
+
};
|
|
107
|
+
const onClick = event => {
|
|
108
|
+
if (!guard.sawKeyDown && ${trusted}) return;
|
|
109
|
+
const state = safeCheck();
|
|
110
|
+
if (guard.blocked || !state.contextMatches || !state.focused || !(event.target instanceof Node) || !button.contains(event.target)) cancel(event, guard.blocked ?? state);
|
|
111
|
+
else guard.clicked = true;
|
|
112
|
+
};
|
|
113
|
+
guard.cleanup = () => {
|
|
114
|
+
window.removeEventListener('keydown', onKey, true);
|
|
115
|
+
window.removeEventListener('keyup', onKey, true);
|
|
116
|
+
window.removeEventListener('click', onClick, true);
|
|
117
|
+
};
|
|
118
|
+
const guards = window.__oracleAttachmentPlusGuards ??= Object.create(null);
|
|
119
|
+
guards[${JSON.stringify(guardId)}] = guard;
|
|
120
|
+
const state = safeCheck();
|
|
121
|
+
if (!state.contextMatches || !state.focused) { guard.blocked = state; return state; }
|
|
122
|
+
window.addEventListener('keydown', onKey, true);
|
|
123
|
+
window.addEventListener('keyup', onKey, true);
|
|
124
|
+
window.addEventListener('click', onClick, true);
|
|
125
|
+
if (!${trusted}) button.click();
|
|
126
|
+
return state;
|
|
127
|
+
})()`,
|
|
128
|
+
returnByValue: true,
|
|
129
|
+
});
|
|
130
|
+
const state = prepared.result?.value;
|
|
131
|
+
assertComposerNavigationSnapshot(startUrl, state);
|
|
132
|
+
if (!state?.focused)
|
|
133
|
+
throw new BrowserAutomationError("Attachment menu focus changed before activation; upload was stopped.", { stage: "upload-attachment", code: "attachment-plus-not-ready" });
|
|
134
|
+
if (trusted && input) {
|
|
135
|
+
const enter = {
|
|
136
|
+
key: "Enter",
|
|
137
|
+
code: "Enter",
|
|
138
|
+
windowsVirtualKeyCode: 13,
|
|
139
|
+
nativeVirtualKeyCode: 13,
|
|
140
|
+
};
|
|
141
|
+
// A transport failure can follow a successful dispatch; never activate twice.
|
|
142
|
+
await input.dispatchKeyEvent({ type: "keyDown", ...enter, text: "\r", unmodifiedText: "\r" });
|
|
143
|
+
await input.dispatchKeyEvent({ type: "keyUp", ...enter });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
const observed = await withoutBrowserCancellation(() => runtime
|
|
148
|
+
.evaluate({
|
|
149
|
+
expression: `(() => {
|
|
150
|
+
const guards = window.__oracleAttachmentPlusGuards;
|
|
151
|
+
const guard = guards?.[${JSON.stringify(guardId)}];
|
|
152
|
+
const nodes = window.__oracleAttachmentPlusNodes;
|
|
153
|
+
if (nodes) delete nodes[${JSON.stringify(guardId)}];
|
|
154
|
+
if (!guard) return null;
|
|
155
|
+
const summary = { sawKeyDown: guard.sawKeyDown, clicked: guard.clicked, blocked: guard.blocked };
|
|
156
|
+
guard.cleanup(); delete guards[${JSON.stringify(guardId)}]; return summary;
|
|
157
|
+
})()`,
|
|
158
|
+
returnByValue: true,
|
|
159
|
+
})
|
|
160
|
+
.catch(() => undefined));
|
|
161
|
+
delivery = observed?.result?.value;
|
|
162
|
+
}
|
|
163
|
+
if (delivery?.blocked) {
|
|
164
|
+
assertComposerNavigationSnapshot(startUrl, delivery.blocked);
|
|
165
|
+
throw new BrowserAutomationError("Attachment menu focus changed at activation; upload was stopped.", { stage: "upload-attachment", code: "attachment-plus-not-ready" });
|
|
166
|
+
}
|
|
167
|
+
if (!delivery || (trusted ? !delivery.sawKeyDown : !delivery.clicked)) {
|
|
168
|
+
throw new BrowserAutomationError("Attachment menu activation could not be verified; do not retry automatically.", { stage: "upload-attachment", code: "attachment-plus-ambiguous" });
|
|
169
|
+
}
|
|
170
|
+
return { method: trusted ? "trusted-keyboard" : "synthetic", startUrl };
|
|
171
|
+
}
|
|
7
172
|
export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
8
173
|
const { runtime, dom, input } = deps;
|
|
9
174
|
if (!dom) {
|
|
@@ -13,17 +178,19 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
13
178
|
? Math.max(0, Math.floor(options.expectedCount))
|
|
14
179
|
: 0;
|
|
15
180
|
const readAttachmentSignals = async (name) => {
|
|
181
|
+
const namePattern = buildAttachmentNamePattern(name, true);
|
|
16
182
|
const check = await runtime.evaluate({
|
|
17
183
|
expression: `(() => {
|
|
18
184
|
const expected = ${JSON.stringify(name)};
|
|
19
185
|
const normalizedExpected = String(expected || '').toLowerCase().replace(/\\s+/g, ' ').trim();
|
|
20
186
|
const expectedNoExt = normalizedExpected.replace(/\\.[a-z0-9]{1,10}$/i, '');
|
|
187
|
+
const namePatternSource = ${JSON.stringify(namePattern?.source ?? "")};
|
|
188
|
+
const namePattern = namePatternSource ? new RegExp(namePatternSource, 'iu') : null;
|
|
21
189
|
const normalize = (value) => String(value || '').toLowerCase().replace(/\\s+/g, ' ').trim();
|
|
22
190
|
const matchesExpected = (value) => {
|
|
23
191
|
const text = normalize(value);
|
|
24
192
|
if (!text) return false;
|
|
25
|
-
if (text
|
|
26
|
-
if (expectedNoExt.length >= 6 && text.includes(expectedNoExt)) return true;
|
|
193
|
+
if (namePattern?.test(text)) return true;
|
|
27
194
|
if (text.includes('…') || text.includes('...')) {
|
|
28
195
|
const marker = text.includes('…') ? '…' : '...';
|
|
29
196
|
const [prefixRaw, suffixRaw] = text.split(marker);
|
|
@@ -237,22 +404,23 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
237
404
|
'[aria-label*="attachment"]',
|
|
238
405
|
'[title*="attachment"]',
|
|
239
406
|
].join(',');
|
|
240
|
-
const fileCountScope =
|
|
407
|
+
const fileCountScope =
|
|
408
|
+
scope && scope !== document.body
|
|
409
|
+
? scope
|
|
410
|
+
: root && root !== document.body
|
|
411
|
+
? root
|
|
412
|
+
: null;
|
|
241
413
|
const localFileNodes = fileCountScope
|
|
242
414
|
? Array.from(fileCountScope.querySelectorAll(fileCountSelectors))
|
|
243
415
|
: [];
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
if (!fileCount && globalFileNodes.length > 0) {
|
|
247
|
-
fileCount = collectFileCount(globalFileNodes);
|
|
248
|
-
}
|
|
249
|
-
const hasAttachmentSignal = localCandidates.length > 0 || inputCount > 0 || fileCount > 0 || uploading;
|
|
416
|
+
const fileCount = collectFileCount(localFileNodes);
|
|
417
|
+
const hasAttachmentSignal = localCandidates.length > 0 || inputCount > 0 || uploading;
|
|
250
418
|
if (!uiMatch && rootTextRaw && hasAttachmentSignal && matchesExpected(rootTextRaw)) {
|
|
251
419
|
uiMatch = true;
|
|
252
420
|
}
|
|
253
421
|
|
|
254
422
|
return {
|
|
255
|
-
ui: uiMatch,
|
|
423
|
+
ui: uiMatch || (${buildAttachmentEvidenceExpression([name])})[0],
|
|
256
424
|
input: inputMatch,
|
|
257
425
|
inputCount,
|
|
258
426
|
chipCount: localCandidates.length,
|
|
@@ -274,87 +442,24 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
274
442
|
fileCount: typeof value?.fileCount === "number" ? value?.fileCount : 0,
|
|
275
443
|
};
|
|
276
444
|
};
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
const locate = await runtime
|
|
283
|
-
.evaluate({
|
|
284
|
-
expression: `(() => {
|
|
285
|
-
const selectors = [
|
|
286
|
-
'#composer-plus-btn',
|
|
287
|
-
'button[data-testid="composer-plus-btn"]',
|
|
288
|
-
'[data-testid*="plus"]',
|
|
289
|
-
'button[aria-label*="add"]',
|
|
290
|
-
'button[aria-label*="attachment"]',
|
|
291
|
-
'button[aria-label*="file"]',
|
|
292
|
-
];
|
|
293
|
-
for (const selector of selectors) {
|
|
294
|
-
const el = document.querySelector(selector);
|
|
295
|
-
if (!(el instanceof HTMLElement)) continue;
|
|
296
|
-
const rect = el.getBoundingClientRect();
|
|
297
|
-
if (rect.width <= 0 || rect.height <= 0) continue;
|
|
298
|
-
el.scrollIntoView({ block: 'center', inline: 'center' });
|
|
299
|
-
const nextRect = el.getBoundingClientRect();
|
|
300
|
-
return { ok: true, x: nextRect.left + nextRect.width / 2, y: nextRect.top + nextRect.height / 2 };
|
|
301
|
-
}
|
|
302
|
-
return { ok: false };
|
|
303
|
-
})()`,
|
|
304
|
-
returnByValue: true,
|
|
305
|
-
})
|
|
306
|
-
.then((res) => res?.result?.value)
|
|
307
|
-
.catch(() => undefined);
|
|
308
|
-
if (!locate?.ok || typeof locate.x !== "number" || typeof locate.y !== "number")
|
|
309
|
-
return false;
|
|
310
|
-
const x = locate.x;
|
|
311
|
-
const y = locate.y;
|
|
312
|
-
await input.dispatchMouseEvent({ type: "mouseMoved", x, y });
|
|
313
|
-
await input.dispatchMouseEvent({ type: "mousePressed", x, y, button: "left", clickCount: 1 });
|
|
314
|
-
await input.dispatchMouseEvent({ type: "mouseReleased", x, y, button: "left", clickCount: 1 });
|
|
315
|
-
return true;
|
|
316
|
-
};
|
|
317
|
-
const clickedTrusted = await clickPlusTrusted().catch(() => false);
|
|
318
|
-
if (!clickedTrusted) {
|
|
319
|
-
await Promise.resolve(runtime.evaluate({
|
|
320
|
-
expression: `(() => {
|
|
321
|
-
const selectors = [
|
|
322
|
-
'#composer-plus-btn',
|
|
323
|
-
'button[data-testid="composer-plus-btn"]',
|
|
324
|
-
'[data-testid*="plus"]',
|
|
325
|
-
'button[aria-label*="add"]',
|
|
326
|
-
'button[aria-label*="attachment"]',
|
|
327
|
-
'button[aria-label*="file"]',
|
|
328
|
-
];
|
|
329
|
-
for (const selector of selectors) {
|
|
330
|
-
const el = document.querySelector(selector);
|
|
331
|
-
if (el instanceof HTMLElement) {
|
|
332
|
-
el.click();
|
|
333
|
-
return true;
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
return false;
|
|
337
|
-
})()`,
|
|
338
|
-
returnByValue: true,
|
|
339
|
-
})).catch(() => undefined);
|
|
340
|
-
}
|
|
445
|
+
// Work suggestions sit close to the composer. Activate only the exact plus control and
|
|
446
|
+
// verify that the page did not enter Work or another conversation before touching files.
|
|
447
|
+
const plusActivation = await activateComposerPlus(runtime, input, options?.navigationUrl);
|
|
448
|
+
const navigationUrl = options?.navigationUrl ?? plusActivation.startUrl;
|
|
449
|
+
const assertNavigation = () => assertComposerPlusStayedInPlace(runtime, navigationUrl);
|
|
341
450
|
await delay(350);
|
|
451
|
+
await assertNavigation();
|
|
342
452
|
const normalizeForMatch = (value) => String(value || "")
|
|
343
453
|
.toLowerCase()
|
|
344
454
|
.replace(/\s+/g, " ")
|
|
345
455
|
.trim();
|
|
346
456
|
const expectedName = path.basename(attachment.path);
|
|
347
|
-
const
|
|
348
|
-
const expectedNameNoExt = expectedNameLower.replace(/\.[a-z0-9]{1,10}$/i, "");
|
|
457
|
+
const expectedNamePattern = buildAttachmentNamePattern(expectedName, true);
|
|
349
458
|
const matchesExpectedName = (value) => {
|
|
350
459
|
const normalized = normalizeForMatch(value);
|
|
351
460
|
if (!normalized)
|
|
352
461
|
return false;
|
|
353
|
-
|
|
354
|
-
return true;
|
|
355
|
-
if (expectedNameNoExt.length >= 6 && normalized.includes(expectedNameNoExt))
|
|
356
|
-
return true;
|
|
357
|
-
return false;
|
|
462
|
+
return expectedNamePattern?.test(normalized) ?? false;
|
|
358
463
|
};
|
|
359
464
|
const isImageAttachment = /\.(png|jpe?g|gif|webp|bmp|svg|heic|heif)$/i.test(expectedName);
|
|
360
465
|
const attachmentUiTimeoutMs = 25_000;
|
|
@@ -368,20 +473,10 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
368
473
|
const isExpectedSatisfied = (signals) => {
|
|
369
474
|
if (expectedCount <= 0)
|
|
370
475
|
return false;
|
|
371
|
-
const fileCount = typeof signals.fileCount === "number" ? signals.fileCount : 0;
|
|
372
476
|
const chipCount = typeof signals.chipCount === "number" ? signals.chipCount : 0;
|
|
373
|
-
if (fileCount >= expectedCount)
|
|
374
|
-
return true;
|
|
375
477
|
return Boolean(signals.ui && chipCount >= expectedCount);
|
|
376
478
|
};
|
|
377
|
-
|
|
378
|
-
if (expectedCount > 0 &&
|
|
379
|
-
(initialSignals.fileCount >= expectedCount || initialSignals.inputCount >= expectedCount)) {
|
|
380
|
-
const satisfiedCount = Math.max(initialSignals.fileCount, initialSignals.inputCount);
|
|
381
|
-
logger(`Attachment already present: composer shows ${satisfiedCount} file${satisfiedCount === 1 ? "" : "s"}`);
|
|
382
|
-
return true;
|
|
383
|
-
}
|
|
384
|
-
if (initialInputSatisfied || initialSignals.input) {
|
|
479
|
+
if (initialSignals.input) {
|
|
385
480
|
logger(`Attachment already queued in file input: ${path.basename(attachment.path)}`);
|
|
386
481
|
return true;
|
|
387
482
|
}
|
|
@@ -557,15 +652,16 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
557
652
|
'[aria-label*="attachment"]',
|
|
558
653
|
'[title*="attachment"]',
|
|
559
654
|
].join(',');
|
|
560
|
-
const fileCountScope =
|
|
655
|
+
const fileCountScope =
|
|
656
|
+
scope && scope !== document.body
|
|
657
|
+
? scope
|
|
658
|
+
: root && root !== document.body
|
|
659
|
+
? root
|
|
660
|
+
: null;
|
|
561
661
|
const localFileNodes = fileCountScope
|
|
562
662
|
? Array.from(fileCountScope.querySelectorAll(fileCountSelectors))
|
|
563
663
|
: [];
|
|
564
|
-
const
|
|
565
|
-
let baselineFileCount = collectFileCount(localFileNodes);
|
|
566
|
-
if (!baselineFileCount && globalFileNodes.length > 0) {
|
|
567
|
-
baselineFileCount = collectFileCount(globalFileNodes);
|
|
568
|
-
}
|
|
664
|
+
const baselineFileCount = collectFileCount(localFileNodes);
|
|
569
665
|
|
|
570
666
|
// Mark candidates with stable indices so we can select them via DOM.querySelector.
|
|
571
667
|
// Learned: ChatGPT sometimes renders a zero-sized file input that does *not* trigger uploads;
|
|
@@ -618,7 +714,6 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
618
714
|
? candidateValue.baselineChips
|
|
619
715
|
: [];
|
|
620
716
|
const baselineUploading = Boolean(candidateValue?.baselineUploading);
|
|
621
|
-
const baselineFileCount = typeof candidateValue?.baselineFileCount === "number" ? candidateValue.baselineFileCount : 0;
|
|
622
717
|
const baselineInputCount = typeof candidateValue?.baselineInputCount === "number" ? candidateValue.baselineInputCount : 0;
|
|
623
718
|
const serializeChips = (chips) => chips
|
|
624
719
|
.map((chip) => [chip.text, chip.aria, chip.title, chip.testid]
|
|
@@ -644,7 +739,6 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
644
739
|
};
|
|
645
740
|
const hasInputDelta = (signals) => (typeof signals.inputCount === "number" ? signals.inputCount : 0) > baselineInputCount;
|
|
646
741
|
const hasUploadDelta = (signals) => Boolean(signals.uploading && !baselineUploading);
|
|
647
|
-
const hasFileCountDelta = (signals) => (typeof signals.fileCount === "number" ? signals.fileCount : 0) > baselineFileCount;
|
|
648
742
|
const waitForAttachmentUiSignal = async (timeoutMs) => {
|
|
649
743
|
const deadline = Date.now() + timeoutMs;
|
|
650
744
|
let sawInputSignal = false;
|
|
@@ -654,7 +748,6 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
654
748
|
const chipDelta = hasChipDelta(signals);
|
|
655
749
|
const inputDelta = hasInputDelta(signals) || signals.input;
|
|
656
750
|
const uploadDelta = hasUploadDelta(signals);
|
|
657
|
-
const fileCountDelta = hasFileCountDelta(signals);
|
|
658
751
|
const expectedSatisfied = isExpectedSatisfied(signals);
|
|
659
752
|
if (inputDelta) {
|
|
660
753
|
sawInputSignal = true;
|
|
@@ -664,10 +757,9 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
664
757
|
chipDelta,
|
|
665
758
|
inputDelta: sawInputSignal,
|
|
666
759
|
uploadDelta,
|
|
667
|
-
fileCountDelta,
|
|
668
760
|
expectedSatisfied,
|
|
669
761
|
};
|
|
670
|
-
if (signals.ui || chipDelta || uploadDelta ||
|
|
762
|
+
if (signals.ui || chipDelta || uploadDelta || expectedSatisfied) {
|
|
671
763
|
return latest;
|
|
672
764
|
}
|
|
673
765
|
await delay(250);
|
|
@@ -817,9 +909,26 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
817
909
|
};
|
|
818
910
|
})()`;
|
|
819
911
|
let confirmedAttachment = false;
|
|
912
|
+
let uploadEvidenceConfirmed = false;
|
|
913
|
+
let exactInputNameConfirmed = false;
|
|
820
914
|
let lastInputNames = [];
|
|
821
915
|
let lastInputValue = "";
|
|
822
916
|
let finalSnapshot = null;
|
|
917
|
+
const readComposerSnapshot = async (idx) => {
|
|
918
|
+
const snapshot = await runtime
|
|
919
|
+
.evaluate({ expression: composerSnapshotFor(idx), returnByValue: true })
|
|
920
|
+
.then((res) => res?.result?.value)
|
|
921
|
+
.catch(() => undefined);
|
|
922
|
+
if (!snapshot)
|
|
923
|
+
return undefined;
|
|
924
|
+
return {
|
|
925
|
+
chipCount: Number(snapshot.chipCount ?? 0),
|
|
926
|
+
chips: Array.isArray(snapshot.chips) ? snapshot.chips : [],
|
|
927
|
+
inputNames: Array.isArray(snapshot.inputNames) ? snapshot.inputNames : [],
|
|
928
|
+
composerText: typeof snapshot.composerText === "string" ? snapshot.composerText : "",
|
|
929
|
+
uploading: Boolean(snapshot.uploading),
|
|
930
|
+
};
|
|
931
|
+
};
|
|
823
932
|
const resolveInputNameCandidates = () => {
|
|
824
933
|
const snapshot = finalSnapshot;
|
|
825
934
|
const snapshotNames = snapshot?.inputNames;
|
|
@@ -835,8 +944,7 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
835
944
|
if (queuedSignals.ui ||
|
|
836
945
|
isExpectedSatisfied(queuedSignals) ||
|
|
837
946
|
hasChipDelta(queuedSignals) ||
|
|
838
|
-
hasUploadDelta(queuedSignals)
|
|
839
|
-
hasFileCountDelta(queuedSignals)) {
|
|
947
|
+
hasUploadDelta(queuedSignals)) {
|
|
840
948
|
confirmedAttachment = true;
|
|
841
949
|
break;
|
|
842
950
|
}
|
|
@@ -856,18 +964,9 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
856
964
|
const signalResult = await waitForAttachmentUiSignal(waitMs);
|
|
857
965
|
const postInputSnapshot = await readInputSnapshot(idx);
|
|
858
966
|
const postInputSignals = inputSignalsFor(baselineInputSnapshot, postInputSnapshot);
|
|
859
|
-
const snapshot = await
|
|
860
|
-
.evaluate({ expression: composerSnapshotFor(idx), returnByValue: true })
|
|
861
|
-
.then((res) => res?.result?.value)
|
|
862
|
-
.catch(() => undefined);
|
|
967
|
+
const snapshot = await readComposerSnapshot(idx);
|
|
863
968
|
if (snapshot) {
|
|
864
|
-
finalSnapshot =
|
|
865
|
-
chipCount: Number(snapshot.chipCount ?? 0),
|
|
866
|
-
chips: Array.isArray(snapshot.chips) ? snapshot.chips : [],
|
|
867
|
-
inputNames: Array.isArray(snapshot.inputNames) ? snapshot.inputNames : [],
|
|
868
|
-
composerText: typeof snapshot.composerText === "string" ? snapshot.composerText : "",
|
|
869
|
-
uploading: Boolean(snapshot.uploading),
|
|
870
|
-
};
|
|
969
|
+
finalSnapshot = snapshot;
|
|
871
970
|
}
|
|
872
971
|
lastInputNames = postInputSnapshot.names;
|
|
873
972
|
lastInputValue = postInputSnapshot.value;
|
|
@@ -879,25 +978,23 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
879
978
|
const inputNameCandidates = resolveInputNameCandidates();
|
|
880
979
|
const inputHasFile = inputNameCandidates.some((name) => matchesExpectedName(name)) ||
|
|
881
980
|
(lastInputValue && matchesExpectedName(lastInputValue));
|
|
981
|
+
if (inputHasFile) {
|
|
982
|
+
exactInputNameConfirmed = true;
|
|
983
|
+
}
|
|
882
984
|
const inputEvidence = immediateInputMatch ||
|
|
883
985
|
postInputSignals.touched ||
|
|
884
986
|
Boolean(signalResult?.signals?.input) ||
|
|
885
987
|
Boolean(signalResult?.inputDelta) ||
|
|
886
988
|
inputHasFile;
|
|
887
989
|
const uiDirect = Boolean(signalResult?.signals?.ui) || expectedSatisfied;
|
|
888
|
-
const uiDelta = Boolean(signalResult?.chipDelta) ||
|
|
889
|
-
Boolean(signalResult?.uploadDelta) ||
|
|
890
|
-
Boolean(signalResult?.fileCountDelta);
|
|
990
|
+
const uiDelta = Boolean(signalResult?.chipDelta) || Boolean(signalResult?.uploadDelta);
|
|
891
991
|
if (uiDirect || (uiDelta && inputEvidence)) {
|
|
892
992
|
return { status: "ui" };
|
|
893
993
|
}
|
|
894
994
|
const postSignals = await readAttachmentSignals(expectedName);
|
|
895
995
|
if (postSignals.ui ||
|
|
896
996
|
isExpectedSatisfied(postSignals) ||
|
|
897
|
-
((hasChipDelta(postSignals) ||
|
|
898
|
-
hasUploadDelta(postSignals) ||
|
|
899
|
-
hasFileCountDelta(postSignals)) &&
|
|
900
|
-
inputEvidence)) {
|
|
997
|
+
((hasChipDelta(postSignals) || hasUploadDelta(postSignals)) && inputEvidence)) {
|
|
901
998
|
return { status: "ui" };
|
|
902
999
|
}
|
|
903
1000
|
const inputSignal = immediateInputMatch ||
|
|
@@ -915,16 +1012,24 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
915
1012
|
const runInputAttempt = async (mode) => {
|
|
916
1013
|
let immediateInputSnapshot = await readInputSnapshot(idx);
|
|
917
1014
|
let hasExpectedFile = snapshotMatchesExpected(immediateInputSnapshot);
|
|
1015
|
+
let attemptIssued = false;
|
|
1016
|
+
let evidenceId;
|
|
918
1017
|
if (!hasExpectedFile) {
|
|
1018
|
+
await assertNavigation();
|
|
1019
|
+
evidenceId = await beginAttachmentEvidence(runtime, expectedName);
|
|
919
1020
|
if (mode === "set") {
|
|
920
|
-
await dom.setFileInputFiles({ nodeId: resultNode.nodeId, files: [attachment.path] });
|
|
1021
|
+
await withGuardedFileInput(runtime, `input[type="file"][data-oracle-upload-idx="${idx}"]`, navigationUrl, () => dom.setFileInputFiles({ nodeId: resultNode.nodeId, files: [attachment.path] }));
|
|
1022
|
+
attemptIssued = true;
|
|
921
1023
|
}
|
|
922
1024
|
else {
|
|
923
1025
|
const selector = `input[type="file"][data-oracle-upload-idx="${idx}"]`;
|
|
924
1026
|
try {
|
|
925
|
-
await transferAttachmentViaDataTransfer(runtime, attachment, selector);
|
|
1027
|
+
await transferAttachmentViaDataTransfer(runtime, attachment, selector, navigationUrl);
|
|
1028
|
+
attemptIssued = true;
|
|
926
1029
|
}
|
|
927
1030
|
catch (error) {
|
|
1031
|
+
if (error instanceof BrowserAutomationError)
|
|
1032
|
+
throw error;
|
|
928
1033
|
logger(`Attachment data transfer failed: ${error?.message ?? String(error)}`);
|
|
929
1034
|
}
|
|
930
1035
|
}
|
|
@@ -932,6 +1037,9 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
932
1037
|
hasExpectedFile = snapshotMatchesExpected(immediateInputSnapshot);
|
|
933
1038
|
}
|
|
934
1039
|
const immediateSignals = inputSignalsFor(baselineInputSnapshot, immediateInputSnapshot);
|
|
1040
|
+
if (hasExpectedFile || immediateSignals.nameMatch) {
|
|
1041
|
+
exactInputNameConfirmed = true;
|
|
1042
|
+
}
|
|
935
1043
|
lastInputNames = immediateInputSnapshot.names;
|
|
936
1044
|
lastInputValue = immediateInputSnapshot.value;
|
|
937
1045
|
const immediateInputMatch = immediateSignals.touched || hasExpectedFile;
|
|
@@ -939,26 +1047,16 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
939
1047
|
inputConfirmed = true;
|
|
940
1048
|
}
|
|
941
1049
|
const signalState = await gatherSignals();
|
|
942
|
-
|
|
1050
|
+
if (attemptIssued && evidenceId) {
|
|
1051
|
+
uploadEvidenceConfirmed = await confirmAttachmentEvidence(runtime, evidenceId);
|
|
1052
|
+
}
|
|
1053
|
+
const evaluation = uploadEvidenceConfirmed
|
|
1054
|
+
? { status: "ui" }
|
|
1055
|
+
: await evaluateSignals(signalState.signalResult, signalState.postInputSignals, immediateInputMatch);
|
|
943
1056
|
return { evaluation, signalState, immediateInputMatch };
|
|
944
1057
|
};
|
|
945
1058
|
const dispatchInputEvents = async () => {
|
|
946
|
-
await runtime
|
|
947
|
-
.evaluate({
|
|
948
|
-
expression: `(() => {
|
|
949
|
-
const input = document.querySelector('input[type="file"][data-oracle-upload-idx="${idx}"]');
|
|
950
|
-
if (!(input instanceof HTMLInputElement)) return false;
|
|
951
|
-
try {
|
|
952
|
-
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
953
|
-
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
954
|
-
return true;
|
|
955
|
-
} catch {
|
|
956
|
-
return false;
|
|
957
|
-
}
|
|
958
|
-
})()`,
|
|
959
|
-
returnByValue: true,
|
|
960
|
-
})
|
|
961
|
-
.catch(() => undefined);
|
|
1059
|
+
await dispatchGuardedFileInputEvents(runtime, `input[type="file"][data-oracle-upload-idx="${idx}"]`, navigationUrl);
|
|
962
1060
|
};
|
|
963
1061
|
let result = await runInputAttempt("set");
|
|
964
1062
|
if (result.evaluation.status === "ui") {
|
|
@@ -980,9 +1078,8 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
980
1078
|
break;
|
|
981
1079
|
}
|
|
982
1080
|
logger("Attachment input set; retrying with data transfer to trigger ChatGPT upload.");
|
|
983
|
-
await
|
|
984
|
-
|
|
985
|
-
.catch(() => undefined);
|
|
1081
|
+
await assertNavigation();
|
|
1082
|
+
await clearGuardedFileInput(runtime, `input[type="file"][data-oracle-upload-idx="${idx}"]`, navigationUrl);
|
|
986
1083
|
await delay(150);
|
|
987
1084
|
result = await runInputAttempt("transfer");
|
|
988
1085
|
if (result.evaluation.status === "ui") {
|
|
@@ -999,8 +1096,7 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
999
1096
|
if (lateSignals.ui ||
|
|
1000
1097
|
isExpectedSatisfied(lateSignals) ||
|
|
1001
1098
|
hasChipDelta(lateSignals) ||
|
|
1002
|
-
hasUploadDelta(lateSignals)
|
|
1003
|
-
hasFileCountDelta(lateSignals)) {
|
|
1099
|
+
hasUploadDelta(lateSignals)) {
|
|
1004
1100
|
confirmedAttachment = true;
|
|
1005
1101
|
break;
|
|
1006
1102
|
}
|
|
@@ -1021,28 +1117,40 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
1021
1117
|
break;
|
|
1022
1118
|
}
|
|
1023
1119
|
if (orderIndex < candidateOrder.length - 1) {
|
|
1024
|
-
await
|
|
1025
|
-
|
|
1026
|
-
.catch(() => undefined);
|
|
1120
|
+
await assertNavigation();
|
|
1121
|
+
await clearGuardedFileInput(runtime, `input[type="file"][data-oracle-upload-idx="${idx}"]`, navigationUrl);
|
|
1027
1122
|
await delay(150);
|
|
1028
1123
|
}
|
|
1029
1124
|
}
|
|
1030
1125
|
}
|
|
1126
|
+
if (uploadEvidenceConfirmed) {
|
|
1127
|
+
logger("Attachment queued (per-file composer evidence confirmed)");
|
|
1128
|
+
return true;
|
|
1129
|
+
}
|
|
1031
1130
|
if (confirmedAttachment) {
|
|
1032
1131
|
const inputNameCandidates = resolveInputNameCandidates();
|
|
1033
|
-
const inputHasFile =
|
|
1132
|
+
const inputHasFile = exactInputNameConfirmed ||
|
|
1133
|
+
inputNameCandidates.some((name) => matchesExpectedName(name)) ||
|
|
1034
1134
|
(lastInputValue && matchesExpectedName(lastInputValue));
|
|
1035
|
-
|
|
1135
|
+
// Image previews may expose a new composer-local chip without rendering the filename.
|
|
1136
|
+
// Exact current-input evidence plus the UI delta above is already two-source confirmation;
|
|
1137
|
+
// requiring the literal filename again would reject a successfully queued image.
|
|
1138
|
+
if (!inputHasFile) {
|
|
1139
|
+
await waitForAttachmentVisible(runtime, expectedName, attachmentUiTimeoutMs, logger);
|
|
1140
|
+
}
|
|
1036
1141
|
logger(inputHasFile
|
|
1037
1142
|
? "Attachment queued (UI anchored, file input confirmed)"
|
|
1038
1143
|
: "Attachment queued (UI anchored)");
|
|
1039
1144
|
return true;
|
|
1040
1145
|
}
|
|
1041
1146
|
const inputNameCandidates = resolveInputNameCandidates();
|
|
1042
|
-
const inputHasFile =
|
|
1147
|
+
const inputHasFile = exactInputNameConfirmed ||
|
|
1148
|
+
inputNameCandidates.some((name) => matchesExpectedName(name)) ||
|
|
1043
1149
|
(lastInputValue && matchesExpectedName(lastInputValue));
|
|
1044
1150
|
if (await waitForAttachmentAnchored(runtime, expectedName, attachmentUiTimeoutMs)) {
|
|
1045
|
-
|
|
1151
|
+
if (!inputHasFile) {
|
|
1152
|
+
await waitForAttachmentVisible(runtime, expectedName, attachmentUiTimeoutMs, logger);
|
|
1153
|
+
}
|
|
1046
1154
|
logger(inputHasFile
|
|
1047
1155
|
? "Attachment queued (UI anchored, file input confirmed)"
|
|
1048
1156
|
: "Attachment queued (UI anchored)");
|
|
@@ -1058,6 +1166,7 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
1058
1166
|
export async function clearComposerAttachments(Runtime, timeoutMs, logger) {
|
|
1059
1167
|
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
1060
1168
|
const expression = `(() => {
|
|
1169
|
+
${buildAttachmentEvidenceExpression([], "clear")};
|
|
1061
1170
|
const promptSelectors = ${JSON.stringify(INPUT_SELECTORS)};
|
|
1062
1171
|
const sendSelectors = ${JSON.stringify(SEND_BUTTON_SELECTORS)};
|
|
1063
1172
|
const findPromptNode = () => {
|
|
@@ -1253,19 +1362,7 @@ export async function waitForAttachmentCompletion(Runtime, timeoutMs, expectedNa
|
|
|
1253
1362
|
button.getAttribute('data-disabled') === 'true' ||
|
|
1254
1363
|
window.getComputedStyle(button).pointerEvents === 'none'
|
|
1255
1364
|
: null;
|
|
1256
|
-
const
|
|
1257
|
-
const uploading = uploadingSelectors.some((selector) => {
|
|
1258
|
-
return Array.from(document.querySelectorAll(selector)).some((node) => {
|
|
1259
|
-
const ariaBusy = node.getAttribute?.('aria-busy');
|
|
1260
|
-
const dataState = node.getAttribute?.('data-state');
|
|
1261
|
-
if (ariaBusy === 'true' || dataState === 'loading' || dataState === 'uploading' || dataState === 'pending') {
|
|
1262
|
-
return true;
|
|
1263
|
-
}
|
|
1264
|
-
// Avoid false positives from user prompts ("upload:") or generic UI copy; only treat explicit progress strings as uploading.
|
|
1265
|
-
const text = node.textContent?.toLowerCase?.() ?? '';
|
|
1266
|
-
return /\buploading\b/.test(text) || /\bprocessing\b/.test(text);
|
|
1267
|
-
});
|
|
1268
|
-
});
|
|
1365
|
+
const uploading = ${buildAttachmentProgressExpression("composerRoot")};
|
|
1269
1366
|
const attachmentChipSelectors = [
|
|
1270
1367
|
'[data-testid*="chip"]',
|
|
1271
1368
|
'[data-testid*="attachment"]',
|
|
@@ -1375,14 +1472,16 @@ export async function waitForAttachmentCompletion(Runtime, timeoutMs, expectedNa
|
|
|
1375
1472
|
}
|
|
1376
1473
|
return count;
|
|
1377
1474
|
};
|
|
1378
|
-
const
|
|
1379
|
-
|
|
1475
|
+
const fileCountScope =
|
|
1476
|
+
composerScope && composerScope !== document && composerScope !== document.body
|
|
1477
|
+
? composerScope
|
|
1478
|
+
: null;
|
|
1479
|
+
const localFileCountNodes = fileCountScope
|
|
1480
|
+
? Array.from(fileCountScope.querySelectorAll(fileCountSelectors))
|
|
1380
1481
|
: [];
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
}
|
|
1385
|
-
const filesAttached = attachedNames.length > 0 || fileCount > 0;
|
|
1482
|
+
const fileCount = collectFileCount(localFileCountNodes);
|
|
1483
|
+
const confirmed = ${buildAttachmentEvidenceExpression(expectedNormalized)};
|
|
1484
|
+
const filesAttached = attachedNames.length > 0;
|
|
1386
1485
|
return {
|
|
1387
1486
|
state: button ? (disabled ? 'disabled' : 'ready') : 'missing',
|
|
1388
1487
|
uploading,
|
|
@@ -1390,6 +1489,7 @@ export async function waitForAttachmentCompletion(Runtime, timeoutMs, expectedNa
|
|
|
1390
1489
|
attachedNames,
|
|
1391
1490
|
inputNames,
|
|
1392
1491
|
fileCount,
|
|
1492
|
+
confirmedNames: ${JSON.stringify(expectedNormalized)}.filter((_, index) => confirmed[index]),
|
|
1393
1493
|
};
|
|
1394
1494
|
})()`;
|
|
1395
1495
|
while (Date.now() < deadline) {
|
|
@@ -1420,6 +1520,14 @@ export async function waitForAttachmentCompletion(Runtime, timeoutMs, expectedNa
|
|
|
1420
1520
|
})}`);
|
|
1421
1521
|
}
|
|
1422
1522
|
}
|
|
1523
|
+
if (value.uploading) {
|
|
1524
|
+
attachmentMatchSince = null;
|
|
1525
|
+
inputMatchSince = null;
|
|
1526
|
+
inputOnlyReadySince = null;
|
|
1527
|
+
inputOnlySignature = "";
|
|
1528
|
+
await delay(250);
|
|
1529
|
+
continue;
|
|
1530
|
+
}
|
|
1423
1531
|
const attachedNames = (value.attachedNames ?? [])
|
|
1424
1532
|
.map((name) => name.toLowerCase().replace(/\s+/g, " ").trim())
|
|
1425
1533
|
.filter(Boolean);
|
|
@@ -1427,15 +1535,15 @@ export async function waitForAttachmentCompletion(Runtime, timeoutMs, expectedNa
|
|
|
1427
1535
|
.map((name) => name.toLowerCase().replace(/\s+/g, " ").trim())
|
|
1428
1536
|
.filter(Boolean);
|
|
1429
1537
|
const fileCount = typeof value.fileCount === "number" ? value.fileCount : 0;
|
|
1430
|
-
const fileCountSatisfied = expectedNormalized.length > 0 && fileCount >= expectedNormalized.length;
|
|
1431
1538
|
const matchesExpected = (expected) => {
|
|
1539
|
+
if (value.confirmedNames?.includes(expected))
|
|
1540
|
+
return true;
|
|
1432
1541
|
const baseName = expected.split("/").pop()?.split("\\").pop() ?? expected;
|
|
1433
1542
|
const normalizedExpected = baseName.toLowerCase().replace(/\s+/g, " ").trim();
|
|
1434
1543
|
const expectedNoExt = normalizedExpected.replace(/\.[a-z0-9]{1,10}$/i, "");
|
|
1544
|
+
const namePattern = buildAttachmentNamePattern(normalizedExpected, true);
|
|
1435
1545
|
return attachedNames.some((raw) => {
|
|
1436
|
-
if (raw
|
|
1437
|
-
return true;
|
|
1438
|
-
if (expectedNoExt.length >= 6 && raw.includes(expectedNoExt))
|
|
1546
|
+
if (namePattern?.test(raw))
|
|
1439
1547
|
return true;
|
|
1440
1548
|
if (raw.includes("…") || raw.includes("...")) {
|
|
1441
1549
|
const marker = raw.includes("…") ? "…" : "...";
|
|
@@ -1451,8 +1559,8 @@ export async function waitForAttachmentCompletion(Runtime, timeoutMs, expectedNa
|
|
|
1451
1559
|
});
|
|
1452
1560
|
};
|
|
1453
1561
|
const missing = expectedNormalized.filter((expected) => !matchesExpected(expected));
|
|
1454
|
-
if (missing.length === 0
|
|
1455
|
-
const stableThresholdMs =
|
|
1562
|
+
if (missing.length === 0) {
|
|
1563
|
+
const stableThresholdMs = 1500;
|
|
1456
1564
|
if (attachmentMatchSince === null) {
|
|
1457
1565
|
attachmentMatchSince = Date.now();
|
|
1458
1566
|
}
|
|
@@ -1462,11 +1570,11 @@ export async function waitForAttachmentCompletion(Runtime, timeoutMs, expectedNa
|
|
|
1462
1570
|
}
|
|
1463
1571
|
// Don't treat disabled button as complete - wait for it to become 'ready'.
|
|
1464
1572
|
// The spinner detection is unreliable, so a disabled button likely means upload is in progress.
|
|
1465
|
-
if (value.state === "missing" &&
|
|
1573
|
+
if (value.state === "missing" && value.filesAttached) {
|
|
1466
1574
|
return;
|
|
1467
1575
|
}
|
|
1468
1576
|
// If files are attached but button isn't ready yet, give it more time but don't fail immediately.
|
|
1469
|
-
if (value.filesAttached
|
|
1577
|
+
if (value.filesAttached) {
|
|
1470
1578
|
await delay(500);
|
|
1471
1579
|
continue;
|
|
1472
1580
|
}
|
|
@@ -1496,18 +1604,11 @@ export async function waitForAttachmentCompletion(Runtime, timeoutMs, expectedNa
|
|
|
1496
1604
|
}
|
|
1497
1605
|
// Fallback: if the file input has the expected names, allow progress once that condition is stable.
|
|
1498
1606
|
// Some ChatGPT surfaces only render the filename after sending the message.
|
|
1499
|
-
const inputMissing = expectedNormalized.filter((expected) => {
|
|
1500
|
-
const baseName = expected.split("/").pop()?.split("\\").pop() ?? expected;
|
|
1501
|
-
const normalizedExpected = baseName.toLowerCase().replace(/\s+/g, " ").trim();
|
|
1502
|
-
const expectedNoExt = normalizedExpected.replace(/\.[a-z0-9]{1,10}$/i, "");
|
|
1503
|
-
return !inputNames.some((raw) => raw.includes(normalizedExpected) ||
|
|
1504
|
-
(expectedNoExt.length >= 6 && raw.includes(expectedNoExt)));
|
|
1505
|
-
});
|
|
1506
1607
|
// Don't include 'disabled' - a disabled button likely means upload is still in progress.
|
|
1507
1608
|
const inputStateOk = value.state === "ready" || value.state === "missing";
|
|
1508
|
-
const inputSeenNow =
|
|
1509
|
-
const inputEvidenceOk =
|
|
1510
|
-
const stableThresholdMs =
|
|
1609
|
+
const inputSeenNow = inputOnlyNamesSatisfied;
|
|
1610
|
+
const inputEvidenceOk = inputSeenNow;
|
|
1611
|
+
const stableThresholdMs = 1500;
|
|
1511
1612
|
if (inputSeenNow && inputStateOk && inputEvidenceOk) {
|
|
1512
1613
|
if (inputMatchSince === null) {
|
|
1513
1614
|
inputMatchSince = Date.now();
|
|
@@ -1572,14 +1673,7 @@ export async function waitForUserTurnAttachments(Runtime, expectedNames, timeout
|
|
|
1572
1673
|
const fileCountSatisfied = fileCount >= expectedNormalized.length && expectedNormalized.length > 0;
|
|
1573
1674
|
const attachmentUiSatisfied = attachmentUiCount >= expectedNormalized.length && expectedNormalized.length > 0;
|
|
1574
1675
|
const missing = expectedNormalized.filter((expected) => {
|
|
1575
|
-
|
|
1576
|
-
const normalizedExpected = baseName.toLowerCase().replace(/\s+/g, " ").trim();
|
|
1577
|
-
const expectedNoExt = normalizedExpected.replace(/\.[a-z0-9]{1,10}$/i, "");
|
|
1578
|
-
if (haystack.includes(normalizedExpected))
|
|
1579
|
-
return false;
|
|
1580
|
-
if (expectedNoExt.length >= 6 && haystack.includes(expectedNoExt))
|
|
1581
|
-
return false;
|
|
1582
|
-
return true;
|
|
1676
|
+
return !buildAttachmentNamePattern(expected, true)?.test(haystack);
|
|
1583
1677
|
});
|
|
1584
1678
|
if (promptMatches && (missing.length === 0 || fileCountSatisfied || attachmentUiSatisfied)) {
|
|
1585
1679
|
return true;
|
|
@@ -1705,19 +1799,15 @@ export function buildUserTurnAttachmentExpressionForTest(options) {
|
|
|
1705
1799
|
: null,
|
|
1706
1800
|
});
|
|
1707
1801
|
}
|
|
1708
|
-
export async function waitForAttachmentVisible(Runtime, expectedName, timeoutMs, logger) {
|
|
1802
|
+
export async function waitForAttachmentVisible(Runtime, expectedName, timeoutMs, logger, evidenceId) {
|
|
1709
1803
|
// Attachments can take a few seconds to render in the composer (headless/remote Chrome is slower),
|
|
1710
1804
|
// so respect the caller-provided timeout instead of capping at 2s.
|
|
1711
1805
|
const deadline = Date.now() + timeoutMs;
|
|
1712
1806
|
const expression = `(() => {
|
|
1713
|
-
|
|
1714
|
-
const
|
|
1715
|
-
const normalizedNoExt = normalized.replace(/\\.[a-z0-9]{1,10}$/i, '');
|
|
1807
|
+
if ((${buildAttachmentEvidenceExpression([expectedName])})[0]) return { found: true, source: 'upload-evidence' };
|
|
1808
|
+
const namePattern = new RegExp(${JSON.stringify(buildAttachmentNamePattern(expectedName, true)?.source ?? "(?!)")}, 'iu');
|
|
1716
1809
|
const matchesExpectedFileName = (value) => {
|
|
1717
|
-
|
|
1718
|
-
if (!text) return false;
|
|
1719
|
-
if (text.includes(normalized)) return true;
|
|
1720
|
-
return normalizedNoExt.length >= 6 && text.includes(normalizedNoExt);
|
|
1810
|
+
return namePattern.test(String(value || '').toLowerCase().replace(/\\s+/g, ' ').trim());
|
|
1721
1811
|
};
|
|
1722
1812
|
const matchNode = (node) => {
|
|
1723
1813
|
if (!node) return false;
|
|
@@ -1728,7 +1818,7 @@ export async function waitForAttachmentVisible(Runtime, expectedName, timeoutMs,
|
|
|
1728
1818
|
const testId = node.getAttribute?.('data-testid')?.toLowerCase?.() ?? '';
|
|
1729
1819
|
const alt = node.getAttribute?.('alt')?.toLowerCase?.() ?? '';
|
|
1730
1820
|
const candidates = [text, aria, title, testId, alt].filter(Boolean);
|
|
1731
|
-
return candidates.some(
|
|
1821
|
+
return candidates.some(matchesExpectedFileName);
|
|
1732
1822
|
};
|
|
1733
1823
|
|
|
1734
1824
|
const inputs = Array.from(document.querySelectorAll('input[type="file"]'));
|
|
@@ -1791,93 +1881,25 @@ export async function waitForAttachmentVisible(Runtime, expectedName, timeoutMs,
|
|
|
1791
1881
|
};
|
|
1792
1882
|
const composerRoot = locateComposerRoot() ?? document.body;
|
|
1793
1883
|
|
|
1794
|
-
const attachmentMatch = ['[data-testid*="attachment"]','[data-testid*="chip"]','[data-testid*="upload"]','[data-testid*="file"]'].some((selector) =>
|
|
1884
|
+
const attachmentMatch = ['[data-testid*="attachment"]','[data-testid*="chip"]','[data-testid*="upload"]','[data-testid*="file"]','[aria-label*="Remove" i]'].some((selector) =>
|
|
1795
1885
|
Array.from(composerRoot.querySelectorAll(selector)).some(matchNode),
|
|
1796
1886
|
);
|
|
1797
1887
|
if (attachmentMatch) {
|
|
1798
1888
|
return { found: true, source: 'attachments' };
|
|
1799
1889
|
}
|
|
1800
1890
|
|
|
1801
|
-
const removeButtons = Array.from(
|
|
1802
|
-
(composerRoot ?? document).querySelectorAll('[aria-label*="Remove"],[aria-label*="remove"]'),
|
|
1803
|
-
);
|
|
1804
|
-
const visibleRemove = removeButtons.some((btn) => {
|
|
1805
|
-
if (!(btn instanceof HTMLElement)) return false;
|
|
1806
|
-
const rect = btn.getBoundingClientRect();
|
|
1807
|
-
if (rect.width <= 0 || rect.height <= 0) return false;
|
|
1808
|
-
const style = window.getComputedStyle(btn);
|
|
1809
|
-
return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
|
|
1810
|
-
});
|
|
1811
|
-
if (visibleRemove) {
|
|
1812
|
-
return { found: true, source: 'remove-button' };
|
|
1813
|
-
}
|
|
1814
|
-
|
|
1815
1891
|
const cardTexts = Array.from(composerRoot.querySelectorAll('[aria-label*="Remove"]')).map((btn) =>
|
|
1816
1892
|
btn?.parentElement?.parentElement?.innerText?.toLowerCase?.() ?? '',
|
|
1817
1893
|
);
|
|
1818
|
-
if (cardTexts.some(
|
|
1894
|
+
if (cardTexts.some(matchesExpectedFileName)) {
|
|
1819
1895
|
return { found: true, source: 'attachment-cards' };
|
|
1820
1896
|
}
|
|
1821
1897
|
|
|
1822
|
-
const countRegex = /(?:^|\\b)(\\d+)\\s+(?:files?|attachments?)\\b/;
|
|
1823
|
-
const fileCountNodes = Array.from(composerRoot.querySelectorAll('button,span,div,[aria-label],[title]'));
|
|
1824
|
-
let fileCount = 0;
|
|
1825
|
-
for (const node of fileCountNodes) {
|
|
1826
|
-
if (!(node instanceof HTMLElement)) continue;
|
|
1827
|
-
if (node.matches('textarea,input,[contenteditable="true"]')) continue;
|
|
1828
|
-
const dataTestId = node.getAttribute?.('data-testid') ?? '';
|
|
1829
|
-
const aria = node.getAttribute?.('aria-label') ?? '';
|
|
1830
|
-
const title = node.getAttribute?.('title') ?? '';
|
|
1831
|
-
const tooltip =
|
|
1832
|
-
node.getAttribute?.('data-tooltip') ?? node.getAttribute?.('data-tooltip-content') ?? '';
|
|
1833
|
-
const text = node.textContent ?? '';
|
|
1834
|
-
const parent = node.parentElement;
|
|
1835
|
-
const parentText = parent?.textContent ?? '';
|
|
1836
|
-
const parentAria = parent?.getAttribute?.('aria-label') ?? '';
|
|
1837
|
-
const parentTitle = parent?.getAttribute?.('title') ?? '';
|
|
1838
|
-
const parentTooltip =
|
|
1839
|
-
parent?.getAttribute?.('data-tooltip') ?? parent?.getAttribute?.('data-tooltip-content') ?? '';
|
|
1840
|
-
const parentTestId = parent?.getAttribute?.('data-testid') ?? '';
|
|
1841
|
-
const candidates = [
|
|
1842
|
-
text,
|
|
1843
|
-
aria,
|
|
1844
|
-
title,
|
|
1845
|
-
tooltip,
|
|
1846
|
-
dataTestId,
|
|
1847
|
-
parentText,
|
|
1848
|
-
parentAria,
|
|
1849
|
-
parentTitle,
|
|
1850
|
-
parentTooltip,
|
|
1851
|
-
parentTestId,
|
|
1852
|
-
];
|
|
1853
|
-
let hasFileHint = false;
|
|
1854
|
-
for (const raw of candidates) {
|
|
1855
|
-
if (!raw) continue;
|
|
1856
|
-
const lowered = String(raw).toLowerCase();
|
|
1857
|
-
if (lowered.includes('file') || lowered.includes('attachment')) {
|
|
1858
|
-
hasFileHint = true;
|
|
1859
|
-
break;
|
|
1860
|
-
}
|
|
1861
|
-
}
|
|
1862
|
-
if (!hasFileHint) continue;
|
|
1863
|
-
for (const raw of candidates) {
|
|
1864
|
-
if (!raw) continue;
|
|
1865
|
-
const match = String(raw).toLowerCase().match(countRegex);
|
|
1866
|
-
if (match) {
|
|
1867
|
-
const count = Number(match[1]);
|
|
1868
|
-
if (Number.isFinite(count)) {
|
|
1869
|
-
fileCount = Math.max(fileCount, count);
|
|
1870
|
-
}
|
|
1871
|
-
}
|
|
1872
|
-
}
|
|
1873
|
-
}
|
|
1874
|
-
if (fileCount > 0) {
|
|
1875
|
-
return { found: true, source: 'file-count' };
|
|
1876
|
-
}
|
|
1877
|
-
|
|
1878
1898
|
return { found: false };
|
|
1879
1899
|
})()`;
|
|
1880
1900
|
while (Date.now() < deadline) {
|
|
1901
|
+
if (evidenceId)
|
|
1902
|
+
await confirmAttachmentEvidence(Runtime, evidenceId);
|
|
1881
1903
|
const { result } = await Runtime.evaluate({ expression, returnByValue: true });
|
|
1882
1904
|
const value = result?.value;
|
|
1883
1905
|
if (value?.found) {
|
|
@@ -1894,11 +1916,11 @@ async function waitForAttachmentAnchored(Runtime, expectedName, timeoutMs) {
|
|
|
1894
1916
|
const expression = `(() => {
|
|
1895
1917
|
const normalized = ${JSON.stringify(expectedName.toLowerCase())};
|
|
1896
1918
|
const normalizedNoExt = normalized.replace(/\\.[a-z0-9]{1,10}$/i, '');
|
|
1919
|
+
const namePattern = new RegExp(${JSON.stringify(buildAttachmentNamePattern(expectedName, true)?.source ?? "(?!)")}, 'iu');
|
|
1897
1920
|
const matchesExpected = (value) => {
|
|
1898
|
-
const text = (value ?? '').toLowerCase();
|
|
1921
|
+
const text = String(value ?? '').toLowerCase().replace(/\\s+/g, ' ').trim();
|
|
1899
1922
|
if (!text) return false;
|
|
1900
|
-
if (
|
|
1901
|
-
if (normalizedNoExt.length >= 6 && text.includes(normalizedNoExt)) return true;
|
|
1923
|
+
if (namePattern.test(text)) return true;
|
|
1902
1924
|
if (text.includes('…') || text.includes('...')) {
|
|
1903
1925
|
const marker = text.includes('…') ? '…' : '...';
|
|
1904
1926
|
const [prefixRaw, suffixRaw] = text.split(marker);
|
|
@@ -1948,77 +1970,6 @@ async function waitForAttachmentAnchored(Runtime, expectedName, timeoutMs) {
|
|
|
1948
1970
|
if (cards.some(matchesExpected)) {
|
|
1949
1971
|
return { found: true, text: cards.find(matchesExpected) };
|
|
1950
1972
|
}
|
|
1951
|
-
const countRegex = /(?:^|\\b)(\\d+)\\s+(?:files?|attachments?)\\b/;
|
|
1952
|
-
const fileCountNodes = (() => {
|
|
1953
|
-
const nodes = [];
|
|
1954
|
-
const seen = new Set();
|
|
1955
|
-
const add = (node) => {
|
|
1956
|
-
if (!node || seen.has(node)) return;
|
|
1957
|
-
seen.add(node);
|
|
1958
|
-
nodes.push(node);
|
|
1959
|
-
};
|
|
1960
|
-
const root =
|
|
1961
|
-
document.querySelector('[data-testid*="composer"]') || document.querySelector('form') || document.body;
|
|
1962
|
-
const localNodes = root ? Array.from(root.querySelectorAll('button,span,div,[aria-label],[title]')) : [];
|
|
1963
|
-
for (const node of localNodes) add(node);
|
|
1964
|
-
for (const node of Array.from(document.querySelectorAll('button,span,div,[aria-label],[title]'))) {
|
|
1965
|
-
add(node);
|
|
1966
|
-
}
|
|
1967
|
-
return nodes;
|
|
1968
|
-
})();
|
|
1969
|
-
let fileCount = 0;
|
|
1970
|
-
for (const node of fileCountNodes) {
|
|
1971
|
-
if (!(node instanceof HTMLElement)) continue;
|
|
1972
|
-
if (node.matches('textarea,input,[contenteditable="true"]')) continue;
|
|
1973
|
-
const dataTestId = node.getAttribute?.('data-testid') ?? '';
|
|
1974
|
-
const aria = node.getAttribute?.('aria-label') ?? '';
|
|
1975
|
-
const title = node.getAttribute?.('title') ?? '';
|
|
1976
|
-
const tooltip =
|
|
1977
|
-
node.getAttribute?.('data-tooltip') ?? node.getAttribute?.('data-tooltip-content') ?? '';
|
|
1978
|
-
const text = node.textContent ?? '';
|
|
1979
|
-
const parent = node.parentElement;
|
|
1980
|
-
const parentText = parent?.textContent ?? '';
|
|
1981
|
-
const parentAria = parent?.getAttribute?.('aria-label') ?? '';
|
|
1982
|
-
const parentTitle = parent?.getAttribute?.('title') ?? '';
|
|
1983
|
-
const parentTooltip =
|
|
1984
|
-
parent?.getAttribute?.('data-tooltip') ?? parent?.getAttribute?.('data-tooltip-content') ?? '';
|
|
1985
|
-
const parentTestId = parent?.getAttribute?.('data-testid') ?? '';
|
|
1986
|
-
const candidates = [
|
|
1987
|
-
text,
|
|
1988
|
-
aria,
|
|
1989
|
-
title,
|
|
1990
|
-
tooltip,
|
|
1991
|
-
dataTestId,
|
|
1992
|
-
parentText,
|
|
1993
|
-
parentAria,
|
|
1994
|
-
parentTitle,
|
|
1995
|
-
parentTooltip,
|
|
1996
|
-
parentTestId,
|
|
1997
|
-
];
|
|
1998
|
-
let hasFileHint = false;
|
|
1999
|
-
for (const raw of candidates) {
|
|
2000
|
-
if (!raw) continue;
|
|
2001
|
-
const lowered = String(raw).toLowerCase();
|
|
2002
|
-
if (lowered.includes('file') || lowered.includes('attachment')) {
|
|
2003
|
-
hasFileHint = true;
|
|
2004
|
-
break;
|
|
2005
|
-
}
|
|
2006
|
-
}
|
|
2007
|
-
if (!hasFileHint) continue;
|
|
2008
|
-
for (const raw of candidates) {
|
|
2009
|
-
if (!raw) continue;
|
|
2010
|
-
const match = String(raw).toLowerCase().match(countRegex);
|
|
2011
|
-
if (match) {
|
|
2012
|
-
const count = Number(match[1]);
|
|
2013
|
-
if (Number.isFinite(count)) {
|
|
2014
|
-
fileCount = Math.max(fileCount, count);
|
|
2015
|
-
}
|
|
2016
|
-
}
|
|
2017
|
-
}
|
|
2018
|
-
}
|
|
2019
|
-
if (fileCount > 0) {
|
|
2020
|
-
return { found: true, text: 'file-count' };
|
|
2021
|
-
}
|
|
2022
1973
|
return { found: false };
|
|
2023
1974
|
})()`;
|
|
2024
1975
|
while (Date.now() < deadline) {
|