@steipete/oracle 0.9.0 → 0.10.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/LICENSE +1 -1
- package/README.md +61 -48
- package/dist/bin/oracle-cli.js +455 -402
- package/dist/bin/oracle-mcp.js +2 -2
- package/dist/bin/oracle.js +165 -279
- package/dist/scripts/agent-send.js +31 -31
- package/dist/scripts/check.js +6 -6
- package/dist/scripts/debug/extract-chatgpt-response.js +10 -10
- package/dist/scripts/docs-list.js +30 -30
- package/dist/scripts/git-policy.js +25 -23
- package/dist/scripts/run-cli.js +8 -8
- package/dist/scripts/runner.js +203 -195
- package/dist/scripts/test-browser.js +21 -18
- package/dist/scripts/test-remote-chrome.js +20 -20
- package/dist/src/bridge/connection.js +18 -18
- package/dist/src/bridge/userConfigFile.js +7 -7
- package/dist/src/browser/actions/assistantResponse.js +149 -101
- package/dist/src/browser/actions/attachmentDataTransfer.js +49 -47
- package/dist/src/browser/actions/attachments.js +246 -150
- package/dist/src/browser/actions/domEvents.js +2 -2
- package/dist/src/browser/actions/modelSelection.js +275 -117
- package/dist/src/browser/actions/navigation.js +161 -137
- package/dist/src/browser/actions/promptComposer.js +100 -64
- package/dist/src/browser/actions/remoteFileTransfer.js +10 -10
- package/dist/src/browser/actions/thinkingTime.js +207 -110
- package/dist/src/browser/chromeLifecycle.js +62 -60
- package/dist/src/browser/config.js +34 -15
- package/dist/src/browser/constants.js +17 -12
- package/dist/src/browser/cookies.js +19 -19
- package/dist/src/browser/detect.js +62 -62
- package/dist/src/browser/domDebug.js +1 -1
- package/dist/src/browser/index.js +390 -295
- package/dist/src/browser/modelStrategy.js +1 -1
- package/dist/src/browser/pageActions.js +5 -5
- package/dist/src/browser/policies.js +16 -13
- package/dist/src/browser/profileState.js +44 -39
- package/dist/src/browser/prompt.js +72 -42
- package/dist/src/browser/promptSummary.js +5 -5
- package/dist/src/browser/providerDomFlow.js +1 -1
- package/dist/src/browser/providers/chatgptDomProvider.js +9 -9
- package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +51 -42
- package/dist/src/browser/providers/index.js +2 -2
- package/dist/src/browser/reattach.js +67 -34
- package/dist/src/browser/reattachHelpers.js +31 -26
- package/dist/src/browser/sessionRunner.js +37 -25
- package/dist/src/browser/utils.js +9 -9
- package/dist/src/browserMode.js +1 -1
- package/dist/src/cli/bridge/claudeConfig.js +16 -16
- package/dist/src/cli/bridge/client.js +28 -20
- package/dist/src/cli/bridge/codexConfig.js +16 -16
- package/dist/src/cli/bridge/doctor.js +47 -39
- package/dist/src/cli/bridge/host.js +58 -56
- package/dist/src/cli/browserConfig.js +62 -48
- package/dist/src/cli/browserDefaults.js +27 -26
- package/dist/src/cli/bundleWarnings.js +1 -1
- package/dist/src/cli/clipboard.js +11 -2
- package/dist/src/cli/detach.js +2 -2
- package/dist/src/cli/dryRun.js +29 -25
- package/dist/src/cli/duplicatePromptGuard.js +3 -3
- package/dist/src/cli/engine.js +9 -9
- package/dist/src/cli/errorUtils.js +1 -1
- package/dist/src/cli/fileSize.js +3 -3
- package/dist/src/cli/format.js +2 -2
- package/dist/src/cli/help.js +28 -28
- package/dist/src/cli/hiddenAliases.js +3 -3
- package/dist/src/cli/markdownBundle.js +7 -7
- package/dist/src/cli/markdownRenderer.js +15 -15
- package/dist/src/cli/notifier.js +77 -67
- package/dist/src/cli/options.js +127 -106
- package/dist/src/cli/oscUtils.js +1 -1
- package/dist/src/cli/promptRequirement.js +2 -2
- package/dist/src/cli/renderOutput.js +1 -1
- package/dist/src/cli/rootAlias.js +1 -1
- package/dist/src/cli/runOptions.js +32 -28
- package/dist/src/cli/sessionCommand.js +31 -21
- package/dist/src/cli/sessionDisplay.js +95 -81
- package/dist/src/cli/sessionLineage.js +6 -2
- package/dist/src/cli/sessionRunner.js +103 -93
- package/dist/src/cli/sessionTable.js +26 -23
- package/dist/src/cli/stdin.js +22 -0
- package/dist/src/cli/tagline.js +121 -124
- package/dist/src/cli/tui/index.js +139 -128
- package/dist/src/cli/writeOutputPath.js +5 -5
- package/dist/src/config.js +7 -7
- package/dist/src/gemini-web/browserSessionManager.js +19 -15
- package/dist/src/gemini-web/client.js +76 -70
- package/dist/src/gemini-web/executionMode.js +6 -8
- package/dist/src/gemini-web/executor.js +98 -93
- package/dist/src/gemini-web/index.js +1 -1
- package/dist/src/mcp/server.js +16 -12
- package/dist/src/mcp/tools/consult.js +51 -47
- package/dist/src/mcp/tools/sessionResources.js +12 -12
- package/dist/src/mcp/tools/sessions.js +26 -17
- package/dist/src/mcp/types.js +5 -5
- package/dist/src/mcp/utils.js +15 -7
- package/dist/src/oracle/background.js +15 -15
- package/dist/src/oracle/claude.js +53 -25
- package/dist/src/oracle/client.js +50 -41
- package/dist/src/oracle/config.js +96 -66
- package/dist/src/oracle/errors.js +38 -38
- package/dist/src/oracle/files.js +55 -46
- package/dist/src/oracle/finishLine.js +10 -8
- package/dist/src/oracle/format.js +3 -3
- package/dist/src/oracle/gemini.js +37 -33
- package/dist/src/oracle/logging.js +7 -7
- package/dist/src/oracle/markdown.js +28 -28
- package/dist/src/oracle/modelResolver.js +16 -16
- package/dist/src/oracle/multiModelRunner.js +12 -12
- package/dist/src/oracle/oscProgress.js +8 -8
- package/dist/src/oracle/promptAssembly.js +6 -3
- package/dist/src/oracle/request.js +16 -13
- package/dist/src/oracle/run.js +156 -134
- package/dist/src/oracle/runUtils.js +8 -5
- package/dist/src/oracle/tokenEstimate.js +6 -6
- package/dist/src/oracle/tokenStats.js +5 -5
- package/dist/src/oracle/tokenStringifier.js +5 -5
- package/dist/src/oracle.js +12 -12
- package/dist/src/oracleHome.js +3 -3
- package/dist/src/remote/client.js +25 -25
- package/dist/src/remote/health.js +20 -20
- package/dist/src/remote/remoteServiceConfig.js +9 -9
- package/dist/src/remote/server.js +129 -118
- package/dist/src/sessionManager.js +77 -75
- package/dist/src/sessionStore.js +3 -3
- package/dist/src/version.js +10 -10
- package/dist/vendor/oracle-notifier/README.md +2 -0
- package/package.json +66 -62
- package/vendor/oracle-notifier/README.md +2 -0
- 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/profileSync.js +0 -141
|
@@ -1,46 +1,51 @@
|
|
|
1
|
-
import { ANSWER_SELECTORS, ASSISTANT_ROLE_SELECTOR, CONVERSATION_TURN_SELECTOR, COPY_BUTTON_SELECTOR, FINISHED_ACTIONS_SELECTOR, STOP_BUTTON_SELECTOR, } from
|
|
2
|
-
import { delay } from
|
|
3
|
-
import { logDomFailure, logConversationSnapshot, buildConversationDebugExpression } from
|
|
4
|
-
import { buildClickDispatcher } from
|
|
5
|
-
const ASSISTANT_POLL_TIMEOUT_ERROR =
|
|
1
|
+
import { ANSWER_SELECTORS, ASSISTANT_ROLE_SELECTOR, CONVERSATION_TURN_SELECTOR, COPY_BUTTON_SELECTOR, FINISHED_ACTIONS_SELECTOR, STOP_BUTTON_SELECTOR, } from "../constants.js";
|
|
2
|
+
import { delay } from "../utils.js";
|
|
3
|
+
import { logDomFailure, logConversationSnapshot, buildConversationDebugExpression, } from "../domDebug.js";
|
|
4
|
+
import { buildClickDispatcher } from "./domEvents.js";
|
|
5
|
+
const ASSISTANT_POLL_TIMEOUT_ERROR = "assistant-response-watchdog-timeout";
|
|
6
6
|
function isAnswerNowPlaceholderText(normalized) {
|
|
7
7
|
const text = normalized.trim();
|
|
8
8
|
if (!text)
|
|
9
9
|
return false;
|
|
10
10
|
// Learned: "Pro thinking" shows a placeholder turn that contains "Answer now".
|
|
11
11
|
// That is not the final answer and must be ignored in browser automation.
|
|
12
|
-
if (text ===
|
|
12
|
+
if (text === "chatgpt said:" || text === "chatgpt said")
|
|
13
13
|
return true;
|
|
14
|
-
if (text.includes(
|
|
14
|
+
if (text.includes("file upload request") &&
|
|
15
|
+
(text.includes("pro thinking") || text.includes("chatgpt said"))) {
|
|
15
16
|
return true;
|
|
16
17
|
}
|
|
17
|
-
return text.includes(
|
|
18
|
+
return (text.includes("answer now") && (text.includes("pro thinking") || text.includes("chatgpt said")));
|
|
18
19
|
}
|
|
19
|
-
export async function waitForAssistantResponse(Runtime, timeoutMs, logger, minTurnIndex) {
|
|
20
|
+
export async function waitForAssistantResponse(Runtime, timeoutMs, logger, minTurnIndex, expectedConversationId) {
|
|
20
21
|
const start = Date.now();
|
|
21
|
-
logger(
|
|
22
|
+
logger("Waiting for ChatGPT response");
|
|
22
23
|
// Learned: two paths are needed:
|
|
23
24
|
// 1) DOM observer (fast when mutations fire),
|
|
24
25
|
// 2) snapshot poller (fallback when observers miss or JS stalls).
|
|
25
|
-
const expression = buildResponseObserverExpression(timeoutMs, minTurnIndex);
|
|
26
|
-
const evaluationPromise = Runtime.evaluate({
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
const expression = buildResponseObserverExpression(timeoutMs, minTurnIndex, expectedConversationId);
|
|
27
|
+
const evaluationPromise = Runtime.evaluate({
|
|
28
|
+
expression,
|
|
29
|
+
awaitPromise: true,
|
|
30
|
+
returnByValue: true,
|
|
31
|
+
});
|
|
32
|
+
const raceReadyEvaluation = evaluationPromise.then((value) => ({ kind: "evaluation", value }), (error) => {
|
|
33
|
+
throw { source: "evaluation", error };
|
|
29
34
|
});
|
|
30
35
|
// Use AbortController to stop the poller when the evaluation wins the race,
|
|
31
36
|
// preventing abandoned polling loops from consuming resources.
|
|
32
37
|
const pollerAbort = new AbortController();
|
|
33
|
-
const pollerPromise = pollAssistantCompletion(Runtime, timeoutMs, minTurnIndex, pollerAbort.signal).then((value) => ({ kind:
|
|
34
|
-
throw { source:
|
|
38
|
+
const pollerPromise = pollAssistantCompletion(Runtime, timeoutMs, minTurnIndex, expectedConversationId, pollerAbort.signal).then((value) => ({ kind: "poll", value }), (error) => {
|
|
39
|
+
throw { source: "poll", error };
|
|
35
40
|
});
|
|
36
41
|
let evaluation = null;
|
|
37
42
|
try {
|
|
38
43
|
const winner = await Promise.race([raceReadyEvaluation, pollerPromise]);
|
|
39
|
-
if (winner.kind ===
|
|
44
|
+
if (winner.kind === "poll") {
|
|
40
45
|
if (!winner.value) {
|
|
41
|
-
throw { source:
|
|
46
|
+
throw { source: "poll", error: new Error(ASSISTANT_POLL_TIMEOUT_ERROR) };
|
|
42
47
|
}
|
|
43
|
-
logger(
|
|
48
|
+
logger("Captured assistant response via snapshot watchdog");
|
|
44
49
|
evaluationPromise.catch(() => undefined);
|
|
45
50
|
await terminateRuntimeExecution(Runtime);
|
|
46
51
|
return winner.value;
|
|
@@ -50,21 +55,26 @@ export async function waitForAssistantResponse(Runtime, timeoutMs, logger, minTu
|
|
|
50
55
|
evaluation = winner.value;
|
|
51
56
|
}
|
|
52
57
|
catch (wrappedError) {
|
|
53
|
-
if (wrappedError &&
|
|
58
|
+
if (wrappedError &&
|
|
59
|
+
typeof wrappedError === "object" &&
|
|
60
|
+
"source" in wrappedError &&
|
|
61
|
+
"error" in wrappedError) {
|
|
54
62
|
const { source, error } = wrappedError;
|
|
55
|
-
if (source ===
|
|
63
|
+
if (source === "poll" &&
|
|
64
|
+
error instanceof Error &&
|
|
65
|
+
error.message === ASSISTANT_POLL_TIMEOUT_ERROR) {
|
|
56
66
|
evaluation = await evaluationPromise;
|
|
57
67
|
}
|
|
58
|
-
else if (source ===
|
|
68
|
+
else if (source === "poll") {
|
|
59
69
|
throw error;
|
|
60
70
|
}
|
|
61
|
-
else if (source ===
|
|
62
|
-
const recovered = await recoverAssistantResponse(Runtime, timeoutMs, logger, minTurnIndex);
|
|
71
|
+
else if (source === "evaluation") {
|
|
72
|
+
const recovered = await recoverAssistantResponse(Runtime, timeoutMs, logger, minTurnIndex, expectedConversationId);
|
|
63
73
|
if (recovered) {
|
|
64
74
|
return recovered;
|
|
65
75
|
}
|
|
66
|
-
await logDomFailure(Runtime, logger,
|
|
67
|
-
throw error ?? new Error(
|
|
76
|
+
await logDomFailure(Runtime, logger, "assistant-response");
|
|
77
|
+
throw error ?? new Error("Failed to capture assistant response");
|
|
68
78
|
}
|
|
69
79
|
}
|
|
70
80
|
else {
|
|
@@ -72,14 +82,14 @@ export async function waitForAssistantResponse(Runtime, timeoutMs, logger, minTu
|
|
|
72
82
|
}
|
|
73
83
|
}
|
|
74
84
|
if (!evaluation) {
|
|
75
|
-
await logDomFailure(Runtime, logger,
|
|
76
|
-
throw new Error(
|
|
85
|
+
await logDomFailure(Runtime, logger, "assistant-response");
|
|
86
|
+
throw new Error("Failed to capture assistant response");
|
|
77
87
|
}
|
|
78
88
|
const parsed = await parseAssistantEvaluationResult(Runtime, evaluation, logger);
|
|
79
89
|
if (!parsed) {
|
|
80
90
|
let remainingMs = Math.max(0, timeoutMs - (Date.now() - start));
|
|
81
91
|
if (remainingMs > 0) {
|
|
82
|
-
const recovered = await recoverAssistantResponse(Runtime, remainingMs, logger, minTurnIndex);
|
|
92
|
+
const recovered = await recoverAssistantResponse(Runtime, remainingMs, logger, minTurnIndex, expectedConversationId);
|
|
83
93
|
if (recovered) {
|
|
84
94
|
return recovered;
|
|
85
95
|
}
|
|
@@ -89,15 +99,15 @@ export async function waitForAssistantResponse(Runtime, timeoutMs, logger, minTu
|
|
|
89
99
|
pollerPromise.catch(() => null),
|
|
90
100
|
delay(remainingMs).then(() => null),
|
|
91
101
|
]);
|
|
92
|
-
if (polled && polled.kind ===
|
|
102
|
+
if (polled && polled.kind === "poll" && polled.value) {
|
|
93
103
|
return polled.value;
|
|
94
104
|
}
|
|
95
105
|
}
|
|
96
106
|
}
|
|
97
|
-
await logDomFailure(Runtime, logger,
|
|
98
|
-
throw new Error(
|
|
107
|
+
await logDomFailure(Runtime, logger, "assistant-response");
|
|
108
|
+
throw new Error("Unable to capture assistant response");
|
|
99
109
|
}
|
|
100
|
-
const refreshed = await refreshAssistantSnapshot(Runtime, parsed, logger, minTurnIndex);
|
|
110
|
+
const refreshed = await refreshAssistantSnapshot(Runtime, parsed, logger, minTurnIndex, expectedConversationId);
|
|
101
111
|
const candidate = refreshed ?? parsed;
|
|
102
112
|
// The evaluation path can race ahead of completion. If ChatGPT is still streaming, wait for the watchdog poller.
|
|
103
113
|
const elapsedMs = Date.now() - start;
|
|
@@ -108,8 +118,8 @@ export async function waitForAssistantResponse(Runtime, timeoutMs, logger, minTu
|
|
|
108
118
|
isCompletionVisible(Runtime),
|
|
109
119
|
]);
|
|
110
120
|
if (stopVisible) {
|
|
111
|
-
logger(
|
|
112
|
-
const completed = await pollAssistantCompletion(Runtime, remainingMs, minTurnIndex);
|
|
121
|
+
logger("Assistant still generating; waiting for completion");
|
|
122
|
+
const completed = await pollAssistantCompletion(Runtime, remainingMs, minTurnIndex, expectedConversationId);
|
|
113
123
|
if (completed) {
|
|
114
124
|
return completed;
|
|
115
125
|
}
|
|
@@ -120,16 +130,16 @@ export async function waitForAssistantResponse(Runtime, timeoutMs, logger, minTu
|
|
|
120
130
|
}
|
|
121
131
|
return candidate;
|
|
122
132
|
}
|
|
123
|
-
export async function readAssistantSnapshot(Runtime, minTurnIndex) {
|
|
133
|
+
export async function readAssistantSnapshot(Runtime, minTurnIndex, expectedConversationId) {
|
|
124
134
|
const { result } = await Runtime.evaluate({
|
|
125
|
-
expression: buildAssistantSnapshotExpression(minTurnIndex),
|
|
135
|
+
expression: buildAssistantSnapshotExpression(minTurnIndex, expectedConversationId),
|
|
126
136
|
returnByValue: true,
|
|
127
137
|
});
|
|
128
138
|
const value = result?.value;
|
|
129
|
-
if (value && typeof value ===
|
|
139
|
+
if (value && typeof value === "object") {
|
|
130
140
|
const snapshot = value;
|
|
131
|
-
if (typeof minTurnIndex ===
|
|
132
|
-
const turnIndex = typeof snapshot.turnIndex ===
|
|
141
|
+
if (typeof minTurnIndex === "number" && Number.isFinite(minTurnIndex)) {
|
|
142
|
+
const turnIndex = typeof snapshot.turnIndex === "number" ? snapshot.turnIndex : null;
|
|
133
143
|
if (turnIndex === null) {
|
|
134
144
|
return snapshot;
|
|
135
145
|
}
|
|
@@ -147,42 +157,45 @@ export async function captureAssistantMarkdown(Runtime, meta, logger) {
|
|
|
147
157
|
returnByValue: true,
|
|
148
158
|
awaitPromise: true,
|
|
149
159
|
});
|
|
150
|
-
if (result?.value?.success && typeof result.value.markdown ===
|
|
160
|
+
if (result?.value?.success && typeof result.value.markdown === "string") {
|
|
151
161
|
return result.value.markdown;
|
|
152
162
|
}
|
|
153
163
|
const status = result?.value?.status;
|
|
154
|
-
if (status && status !==
|
|
164
|
+
if (status && status !== "missing-button") {
|
|
155
165
|
logger(`Copy button fallback status: ${status}`);
|
|
156
|
-
await logDomFailure(Runtime, logger,
|
|
166
|
+
await logDomFailure(Runtime, logger, "copy-markdown");
|
|
157
167
|
}
|
|
158
168
|
if (!status) {
|
|
159
|
-
await logDomFailure(Runtime, logger,
|
|
169
|
+
await logDomFailure(Runtime, logger, "copy-markdown");
|
|
160
170
|
}
|
|
161
171
|
return null;
|
|
162
172
|
}
|
|
163
173
|
export function buildAssistantExtractorForTest(name) {
|
|
164
174
|
return buildAssistantExtractor(name);
|
|
165
175
|
}
|
|
176
|
+
export function buildAssistantSnapshotExpressionForTest(minTurnIndex, expectedConversationId) {
|
|
177
|
+
return buildAssistantSnapshotExpression(minTurnIndex, expectedConversationId);
|
|
178
|
+
}
|
|
166
179
|
export function buildConversationDebugExpressionForTest() {
|
|
167
180
|
return buildConversationDebugExpression();
|
|
168
181
|
}
|
|
169
|
-
export function buildMarkdownFallbackExtractorForTest(minTurnLiteral =
|
|
182
|
+
export function buildMarkdownFallbackExtractorForTest(minTurnLiteral = "0") {
|
|
170
183
|
return buildMarkdownFallbackExtractor(minTurnLiteral);
|
|
171
184
|
}
|
|
172
185
|
export function buildCopyExpressionForTest(meta = {}) {
|
|
173
186
|
return buildCopyExpression(meta);
|
|
174
187
|
}
|
|
175
|
-
async function recoverAssistantResponse(Runtime, timeoutMs, logger, minTurnIndex) {
|
|
188
|
+
async function recoverAssistantResponse(Runtime, timeoutMs, logger, minTurnIndex, expectedConversationId) {
|
|
176
189
|
const recoveryTimeoutMs = Math.max(0, timeoutMs);
|
|
177
190
|
if (recoveryTimeoutMs === 0) {
|
|
178
191
|
return null;
|
|
179
192
|
}
|
|
180
193
|
const recovered = await waitForCondition(async () => {
|
|
181
|
-
const snapshot = await readAssistantSnapshot(Runtime, minTurnIndex);
|
|
194
|
+
const snapshot = await readAssistantSnapshot(Runtime, minTurnIndex, expectedConversationId);
|
|
182
195
|
return normalizeAssistantSnapshot(snapshot);
|
|
183
196
|
}, recoveryTimeoutMs, 400);
|
|
184
197
|
if (recovered) {
|
|
185
|
-
logger(
|
|
198
|
+
logger("Recovered assistant response via polling fallback");
|
|
186
199
|
return recovered;
|
|
187
200
|
}
|
|
188
201
|
await logConversationSnapshot(Runtime, logger).catch(() => undefined);
|
|
@@ -190,24 +203,27 @@ async function recoverAssistantResponse(Runtime, timeoutMs, logger, minTurnIndex
|
|
|
190
203
|
}
|
|
191
204
|
async function parseAssistantEvaluationResult(_Runtime, evaluation, _logger) {
|
|
192
205
|
const { result } = evaluation;
|
|
193
|
-
if (result.type ===
|
|
194
|
-
|
|
206
|
+
if (result.type === "object" &&
|
|
207
|
+
result.value &&
|
|
208
|
+
typeof result.value === "object" &&
|
|
209
|
+
"text" in result.value) {
|
|
210
|
+
const html = typeof result.value.html === "string"
|
|
195
211
|
? (result.value.html ?? undefined)
|
|
196
212
|
: undefined;
|
|
197
|
-
const turnId = typeof result.value.turnId ===
|
|
213
|
+
const turnId = typeof result.value.turnId === "string"
|
|
198
214
|
? (result.value.turnId ?? undefined)
|
|
199
215
|
: undefined;
|
|
200
|
-
const messageId = typeof result.value.messageId ===
|
|
216
|
+
const messageId = typeof result.value.messageId === "string"
|
|
201
217
|
? (result.value.messageId ?? undefined)
|
|
202
218
|
: undefined;
|
|
203
|
-
const text = cleanAssistantText(String(result.value.text ??
|
|
219
|
+
const text = cleanAssistantText(String(result.value.text ?? ""));
|
|
204
220
|
const normalized = text.toLowerCase();
|
|
205
221
|
if (isAnswerNowPlaceholderText(normalized)) {
|
|
206
222
|
return null;
|
|
207
223
|
}
|
|
208
224
|
return { text, html, meta: { turnId, messageId } };
|
|
209
225
|
}
|
|
210
|
-
const fallbackText = typeof result.value ===
|
|
226
|
+
const fallbackText = typeof result.value === "string" ? cleanAssistantText(result.value) : "";
|
|
211
227
|
if (!fallbackText) {
|
|
212
228
|
return null;
|
|
213
229
|
}
|
|
@@ -216,14 +232,14 @@ async function parseAssistantEvaluationResult(_Runtime, evaluation, _logger) {
|
|
|
216
232
|
}
|
|
217
233
|
return { text: fallbackText, html: undefined, meta: {} };
|
|
218
234
|
}
|
|
219
|
-
async function refreshAssistantSnapshot(Runtime, current, logger, minTurnIndex) {
|
|
235
|
+
async function refreshAssistantSnapshot(Runtime, current, logger, minTurnIndex, expectedConversationId) {
|
|
220
236
|
const deadline = Date.now() + 5_000;
|
|
221
237
|
let best = null;
|
|
222
238
|
let stableCycles = 0;
|
|
223
239
|
const stableTarget = 3;
|
|
224
240
|
while (Date.now() < deadline) {
|
|
225
241
|
// Learned: short/fast answers can race; poll a few extra cycles to pick up messageId + full text.
|
|
226
|
-
const latestSnapshot = await readAssistantSnapshot(Runtime, minTurnIndex).catch(() => null);
|
|
242
|
+
const latestSnapshot = await readAssistantSnapshot(Runtime, minTurnIndex, expectedConversationId).catch(() => null);
|
|
227
243
|
const latest = normalizeAssistantSnapshot(latestSnapshot);
|
|
228
244
|
if (latest) {
|
|
229
245
|
if (!best ||
|
|
@@ -250,13 +266,13 @@ async function refreshAssistantSnapshot(Runtime, current, logger, minTurnIndex)
|
|
|
250
266
|
const isLonger = latestLength > currentLength;
|
|
251
267
|
const hasDifferentText = best.text.trim() !== current.text.trim();
|
|
252
268
|
if (isLonger || hasBetterId || hasDifferentText) {
|
|
253
|
-
logger(
|
|
269
|
+
logger("Refreshed assistant response via latest snapshot");
|
|
254
270
|
return best;
|
|
255
271
|
}
|
|
256
272
|
return null;
|
|
257
273
|
}
|
|
258
274
|
async function terminateRuntimeExecution(Runtime) {
|
|
259
|
-
if (typeof Runtime.terminateExecution !==
|
|
275
|
+
if (typeof Runtime.terminateExecution !== "function") {
|
|
260
276
|
return;
|
|
261
277
|
}
|
|
262
278
|
try {
|
|
@@ -266,7 +282,7 @@ async function terminateRuntimeExecution(Runtime) {
|
|
|
266
282
|
// ignore termination failures
|
|
267
283
|
}
|
|
268
284
|
}
|
|
269
|
-
async function pollAssistantCompletion(Runtime, timeoutMs, minTurnIndex, abortSignal) {
|
|
285
|
+
async function pollAssistantCompletion(Runtime, timeoutMs, minTurnIndex, expectedConversationId, abortSignal) {
|
|
270
286
|
const watchdogDeadline = Date.now() + timeoutMs;
|
|
271
287
|
let previousLength = 0;
|
|
272
288
|
let stableCycles = 0;
|
|
@@ -276,7 +292,7 @@ async function pollAssistantCompletion(Runtime, timeoutMs, minTurnIndex, abortSi
|
|
|
276
292
|
if (abortSignal?.aborted) {
|
|
277
293
|
return null;
|
|
278
294
|
}
|
|
279
|
-
const snapshot = await readAssistantSnapshot(Runtime, minTurnIndex);
|
|
295
|
+
const snapshot = await readAssistantSnapshot(Runtime, minTurnIndex, expectedConversationId);
|
|
280
296
|
const normalized = normalizeAssistantSnapshot(snapshot);
|
|
281
297
|
if (normalized) {
|
|
282
298
|
const currentLength = normalized.text.length;
|
|
@@ -377,7 +393,7 @@ async function isCompletionVisible(Runtime) {
|
|
|
377
393
|
}
|
|
378
394
|
}
|
|
379
395
|
function normalizeAssistantSnapshot(snapshot) {
|
|
380
|
-
const text = snapshot?.text ? cleanAssistantText(snapshot.text) :
|
|
396
|
+
const text = snapshot?.text ? cleanAssistantText(snapshot.text) : "";
|
|
381
397
|
if (!text.trim()) {
|
|
382
398
|
return null;
|
|
383
399
|
}
|
|
@@ -388,7 +404,7 @@ function normalizeAssistantSnapshot(snapshot) {
|
|
|
388
404
|
return null;
|
|
389
405
|
}
|
|
390
406
|
// Ignore user echo turns that can show up in project view fallbacks.
|
|
391
|
-
if (normalized.startsWith(
|
|
407
|
+
if (normalized.startsWith("you said")) {
|
|
392
408
|
return null;
|
|
393
409
|
}
|
|
394
410
|
return {
|
|
@@ -408,14 +424,27 @@ async function waitForCondition(getter, timeoutMs, pollIntervalMs = 400) {
|
|
|
408
424
|
}
|
|
409
425
|
return null;
|
|
410
426
|
}
|
|
411
|
-
function buildAssistantSnapshotExpression(minTurnIndex) {
|
|
412
|
-
const minTurnLiteral = typeof minTurnIndex ===
|
|
427
|
+
function buildAssistantSnapshotExpression(minTurnIndex, expectedConversationId) {
|
|
428
|
+
const minTurnLiteral = typeof minTurnIndex === "number" && Number.isFinite(minTurnIndex) && minTurnIndex >= 0
|
|
413
429
|
? Math.floor(minTurnIndex)
|
|
414
430
|
: -1;
|
|
431
|
+
const expectedConversationLiteral = typeof expectedConversationId === "string" && expectedConversationId.trim().length > 0
|
|
432
|
+
? JSON.stringify(expectedConversationId.trim())
|
|
433
|
+
: "null";
|
|
415
434
|
return `(() => {
|
|
416
435
|
const MIN_TURN_INDEX = ${minTurnLiteral};
|
|
436
|
+
const EXPECTED_CONVERSATION_ID = ${expectedConversationLiteral};
|
|
437
|
+
const currentHref = typeof location === 'object' && location.href ? location.href : '';
|
|
438
|
+
const currentConversationId = currentHref.match(/\\/c\\/([a-zA-Z0-9-]+)/)?.[1] ?? null;
|
|
439
|
+
if (
|
|
440
|
+
EXPECTED_CONVERSATION_ID &&
|
|
441
|
+
currentConversationId &&
|
|
442
|
+
currentConversationId !== EXPECTED_CONVERSATION_ID
|
|
443
|
+
) {
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
417
446
|
// Learned: the default turn DOM misses project view; keep a fallback extractor.
|
|
418
|
-
${buildAssistantExtractor(
|
|
447
|
+
${buildAssistantExtractor("extractAssistantTurn")}
|
|
419
448
|
const extracted = extractAssistantTurn();
|
|
420
449
|
const isPlaceholder = (snapshot) => {
|
|
421
450
|
const normalized = String(snapshot?.text ?? '').toLowerCase().trim();
|
|
@@ -429,17 +458,20 @@ function buildAssistantSnapshotExpression(minTurnIndex) {
|
|
|
429
458
|
return extracted;
|
|
430
459
|
}
|
|
431
460
|
// Fallback for ChatGPT project view: answers can live outside conversation turns.
|
|
432
|
-
const fallback = ${buildMarkdownFallbackExtractor(
|
|
461
|
+
const fallback = ${buildMarkdownFallbackExtractor("MIN_TURN_INDEX")};
|
|
433
462
|
return fallback ?? extracted;
|
|
434
463
|
})()`;
|
|
435
464
|
}
|
|
436
|
-
function buildResponseObserverExpression(timeoutMs, minTurnIndex) {
|
|
465
|
+
function buildResponseObserverExpression(timeoutMs, minTurnIndex, expectedConversationId) {
|
|
437
466
|
const selectorsLiteral = JSON.stringify(ANSWER_SELECTORS);
|
|
438
467
|
const conversationLiteral = JSON.stringify(CONVERSATION_TURN_SELECTOR);
|
|
439
468
|
const assistantLiteral = JSON.stringify(ASSISTANT_ROLE_SELECTOR);
|
|
440
|
-
const minTurnLiteral = typeof minTurnIndex ===
|
|
469
|
+
const minTurnLiteral = typeof minTurnIndex === "number" && Number.isFinite(minTurnIndex) && minTurnIndex >= 0
|
|
441
470
|
? Math.floor(minTurnIndex)
|
|
442
471
|
: -1;
|
|
472
|
+
const expectedConversationLiteral = typeof expectedConversationId === "string" && expectedConversationId.trim().length > 0
|
|
473
|
+
? JSON.stringify(expectedConversationId.trim())
|
|
474
|
+
: "null";
|
|
443
475
|
return `(() => {
|
|
444
476
|
${buildClickDispatcher()}
|
|
445
477
|
const SELECTORS = ${selectorsLiteral};
|
|
@@ -447,8 +479,18 @@ function buildResponseObserverExpression(timeoutMs, minTurnIndex) {
|
|
|
447
479
|
const FINISHED_SELECTOR = '${FINISHED_ACTIONS_SELECTOR}';
|
|
448
480
|
const CONVERSATION_SELECTOR = ${conversationLiteral};
|
|
449
481
|
const ASSISTANT_SELECTOR = ${assistantLiteral};
|
|
482
|
+
const EXPECTED_CONVERSATION_ID = ${expectedConversationLiteral};
|
|
450
483
|
// Learned: settling avoids capturing mid-stream HTML; keep short.
|
|
451
484
|
const settleDelayMs = 800;
|
|
485
|
+
const currentConversationId = () => {
|
|
486
|
+
const href = typeof location === 'object' && location.href ? location.href : '';
|
|
487
|
+
return href.match(/\\/c\\/([a-zA-Z0-9-]+)/)?.[1] ?? null;
|
|
488
|
+
};
|
|
489
|
+
const matchesExpectedConversation = () => {
|
|
490
|
+
if (!EXPECTED_CONVERSATION_ID) return true;
|
|
491
|
+
const currentId = currentConversationId();
|
|
492
|
+
return !currentId || currentId === EXPECTED_CONVERSATION_ID;
|
|
493
|
+
};
|
|
452
494
|
const isAnswerNowPlaceholder = (snapshot) => {
|
|
453
495
|
const normalized = String(snapshot?.text ?? '').toLowerCase().trim();
|
|
454
496
|
if (normalized === 'chatgpt said:' || normalized === 'chatgpt said') return true;
|
|
@@ -471,12 +513,13 @@ function buildResponseObserverExpression(timeoutMs, minTurnIndex) {
|
|
|
471
513
|
};
|
|
472
514
|
|
|
473
515
|
const MIN_TURN_INDEX = ${minTurnLiteral};
|
|
474
|
-
${buildAssistantExtractor(
|
|
516
|
+
${buildAssistantExtractor("extractFromTurns")}
|
|
475
517
|
// Learned: some layouts (project view) render markdown without assistant turn wrappers.
|
|
476
|
-
const extractFromMarkdownFallback = ${buildMarkdownFallbackExtractor(
|
|
518
|
+
const extractFromMarkdownFallback = ${buildMarkdownFallbackExtractor("MIN_TURN_INDEX")};
|
|
477
519
|
|
|
478
520
|
const acceptSnapshot = (snapshot) => {
|
|
479
521
|
if (!snapshot) return null;
|
|
522
|
+
if (!matchesExpectedConversation()) return null;
|
|
480
523
|
const index = typeof snapshot.turnIndex === 'number' ? snapshot.turnIndex : -1;
|
|
481
524
|
if (MIN_TURN_INDEX >= 0) {
|
|
482
525
|
if (index < 0 || index < MIN_TURN_INDEX) {
|
|
@@ -719,7 +762,9 @@ function buildAssistantExtractor(functionName) {
|
|
|
719
762
|
};`;
|
|
720
763
|
}
|
|
721
764
|
function buildMarkdownFallbackExtractor(minTurnLiteral) {
|
|
722
|
-
const turnIndexValue = minTurnLiteral
|
|
765
|
+
const turnIndexValue = minTurnLiteral
|
|
766
|
+
? `(${minTurnLiteral} >= 0 ? ${minTurnLiteral} : null)`
|
|
767
|
+
: "null";
|
|
723
768
|
return `(() => {
|
|
724
769
|
const __minTurn = ${turnIndexValue};
|
|
725
770
|
const roots = [
|
|
@@ -1025,37 +1070,37 @@ function buildCopyExpression(meta) {
|
|
|
1025
1070
|
})()`;
|
|
1026
1071
|
}
|
|
1027
1072
|
const LANGUAGE_TAGS = new Set([
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1073
|
+
"copy code",
|
|
1074
|
+
"markdown",
|
|
1075
|
+
"bash",
|
|
1076
|
+
"sh",
|
|
1077
|
+
"shell",
|
|
1078
|
+
"javascript",
|
|
1079
|
+
"typescript",
|
|
1080
|
+
"ts",
|
|
1081
|
+
"js",
|
|
1082
|
+
"yaml",
|
|
1083
|
+
"json",
|
|
1084
|
+
"python",
|
|
1085
|
+
"py",
|
|
1086
|
+
"go",
|
|
1087
|
+
"java",
|
|
1088
|
+
"c",
|
|
1089
|
+
"c++",
|
|
1090
|
+
"cpp",
|
|
1091
|
+
"c#",
|
|
1092
|
+
"php",
|
|
1093
|
+
"ruby",
|
|
1094
|
+
"rust",
|
|
1095
|
+
"swift",
|
|
1096
|
+
"kotlin",
|
|
1097
|
+
"html",
|
|
1098
|
+
"css",
|
|
1099
|
+
"sql",
|
|
1100
|
+
"text",
|
|
1056
1101
|
].map((token) => token.toLowerCase()));
|
|
1057
1102
|
function cleanAssistantText(text) {
|
|
1058
|
-
const normalized = text.replace(/\u00a0/g,
|
|
1103
|
+
const normalized = text.replace(/\u00a0/g, " ");
|
|
1059
1104
|
const lines = normalized.split(/\r?\n/);
|
|
1060
1105
|
const filtered = lines.filter((line) => {
|
|
1061
1106
|
const trimmed = line.trim().toLowerCase();
|
|
@@ -1063,5 +1108,8 @@ function cleanAssistantText(text) {
|
|
|
1063
1108
|
return false;
|
|
1064
1109
|
return true;
|
|
1065
1110
|
});
|
|
1066
|
-
return filtered
|
|
1111
|
+
return filtered
|
|
1112
|
+
.join("\n")
|
|
1113
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
1114
|
+
.trim();
|
|
1067
1115
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { readFile } from
|
|
2
|
-
import path from
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
3
|
const MAX_DATA_TRANSFER_BYTES = 20 * 1024 * 1024;
|
|
4
4
|
export async function transferAttachmentViaDataTransfer(runtime, attachment, selector) {
|
|
5
5
|
const fileContent = await readFile(attachment.path);
|
|
6
6
|
if (fileContent.length > MAX_DATA_TRANSFER_BYTES) {
|
|
7
7
|
throw new Error(`Attachment ${path.basename(attachment.path)} is too large for data transfer (${fileContent.length} bytes). Maximum size is ${MAX_DATA_TRANSFER_BYTES} bytes.`);
|
|
8
8
|
}
|
|
9
|
-
const base64Content = fileContent.toString(
|
|
9
|
+
const base64Content = fileContent.toString("base64");
|
|
10
10
|
const fileName = path.basename(attachment.path);
|
|
11
11
|
const mimeType = guessMimeType(fileName);
|
|
12
12
|
const expression = `(() => {
|
|
@@ -77,62 +77,64 @@ export async function transferAttachmentViaDataTransfer(runtime, attachment, sel
|
|
|
77
77
|
})()`;
|
|
78
78
|
const evalResult = await runtime.evaluate({ expression, returnByValue: true });
|
|
79
79
|
if (evalResult.exceptionDetails) {
|
|
80
|
-
const description = evalResult.exceptionDetails.text ??
|
|
80
|
+
const description = evalResult.exceptionDetails.text ?? "JS evaluation failed";
|
|
81
81
|
throw new Error(`Failed to transfer file to browser: ${description}`);
|
|
82
82
|
}
|
|
83
|
-
if (!evalResult.result ||
|
|
84
|
-
|
|
83
|
+
if (!evalResult.result ||
|
|
84
|
+
typeof evalResult.result.value !== "object" ||
|
|
85
|
+
evalResult.result.value == null) {
|
|
86
|
+
throw new Error("Failed to transfer file to browser: unexpected evaluation result");
|
|
85
87
|
}
|
|
86
88
|
const uploadResult = evalResult.result.value;
|
|
87
89
|
if (!uploadResult.success) {
|
|
88
|
-
throw new Error(`Failed to transfer file to browser: ${uploadResult.error ||
|
|
90
|
+
throw new Error(`Failed to transfer file to browser: ${uploadResult.error || "Unknown error"}`);
|
|
89
91
|
}
|
|
90
92
|
return {
|
|
91
93
|
fileName: uploadResult.fileName ?? fileName,
|
|
92
|
-
size: typeof uploadResult.size ===
|
|
94
|
+
size: typeof uploadResult.size === "number" ? uploadResult.size : fileContent.length,
|
|
93
95
|
};
|
|
94
96
|
}
|
|
95
97
|
export function guessMimeType(fileName) {
|
|
96
98
|
const ext = path.extname(fileName).toLowerCase();
|
|
97
99
|
const mimeTypes = {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
100
|
+
".txt": "text/plain",
|
|
101
|
+
".md": "text/markdown",
|
|
102
|
+
".csv": "text/csv",
|
|
103
|
+
".json": "application/json",
|
|
104
|
+
".js": "text/javascript",
|
|
105
|
+
".ts": "text/typescript",
|
|
106
|
+
".jsx": "text/javascript",
|
|
107
|
+
".tsx": "text/typescript",
|
|
108
|
+
".py": "text/x-python",
|
|
109
|
+
".java": "text/x-java",
|
|
110
|
+
".c": "text/x-c",
|
|
111
|
+
".cpp": "text/x-c++",
|
|
112
|
+
".h": "text/x-c",
|
|
113
|
+
".hpp": "text/x-c++",
|
|
114
|
+
".sh": "text/x-sh",
|
|
115
|
+
".bash": "text/x-sh",
|
|
116
|
+
".html": "text/html",
|
|
117
|
+
".css": "text/css",
|
|
118
|
+
".xml": "text/xml",
|
|
119
|
+
".yaml": "text/yaml",
|
|
120
|
+
".yml": "text/yaml",
|
|
121
|
+
".pdf": "application/pdf",
|
|
122
|
+
".doc": "application/msword",
|
|
123
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
124
|
+
".xls": "application/vnd.ms-excel",
|
|
125
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
126
|
+
".ppt": "application/vnd.ms-powerpoint",
|
|
127
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
128
|
+
".png": "image/png",
|
|
129
|
+
".jpg": "image/jpeg",
|
|
130
|
+
".jpeg": "image/jpeg",
|
|
131
|
+
".gif": "image/gif",
|
|
132
|
+
".svg": "image/svg+xml",
|
|
133
|
+
".webp": "image/webp",
|
|
134
|
+
".zip": "application/zip",
|
|
135
|
+
".tar": "application/x-tar",
|
|
136
|
+
".gz": "application/gzip",
|
|
137
|
+
".7z": "application/x-7z-compressed",
|
|
136
138
|
};
|
|
137
|
-
return mimeTypes[ext] ||
|
|
139
|
+
return mimeTypes[ext] || "application/octet-stream";
|
|
138
140
|
}
|