pi-oracle 0.3.4 → 0.4.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/CHANGELOG.md +21 -0
- package/README.md +2 -0
- package/docs/ORACLE_ISOLATED_PI_VALIDATION.md +249 -0
- package/extensions/oracle/index.ts +8 -1
- package/extensions/oracle/lib/commands.ts +11 -24
- package/extensions/oracle/lib/config.ts +5 -0
- package/extensions/oracle/lib/jobs.ts +117 -217
- package/extensions/oracle/lib/locks.ts +41 -209
- package/extensions/oracle/lib/poller.ts +14 -51
- package/extensions/oracle/lib/queue.ts +75 -112
- package/extensions/oracle/lib/runtime.ts +60 -14
- package/extensions/oracle/lib/tools.ts +66 -65
- package/extensions/oracle/shared/job-coordination-helpers.d.mts +84 -0
- package/extensions/oracle/shared/job-coordination-helpers.mjs +168 -0
- package/extensions/oracle/shared/job-lifecycle-helpers.d.mts +130 -0
- package/extensions/oracle/shared/job-lifecycle-helpers.mjs +377 -0
- package/extensions/oracle/shared/job-observability-helpers.d.mts +59 -0
- package/extensions/oracle/shared/job-observability-helpers.mjs +143 -0
- package/extensions/oracle/shared/process-helpers.d.mts +20 -0
- package/extensions/oracle/shared/process-helpers.mjs +128 -0
- package/extensions/oracle/shared/state-coordination-helpers.d.mts +43 -0
- package/extensions/oracle/shared/state-coordination-helpers.mjs +381 -0
- package/extensions/oracle/worker/artifact-heuristics.mjs +5 -0
- package/extensions/oracle/worker/auth-bootstrap.mjs +76 -130
- package/extensions/oracle/worker/auth-cookie-policy.mjs +5 -0
- package/extensions/oracle/worker/auth-flow-helpers.d.mts +41 -0
- package/extensions/oracle/worker/auth-flow-helpers.mjs +165 -0
- package/extensions/oracle/worker/chatgpt-flow-helpers.d.mts +13 -0
- package/extensions/oracle/worker/chatgpt-flow-helpers.mjs +85 -0
- package/extensions/oracle/worker/chatgpt-ui-helpers.mjs +93 -9
- package/extensions/oracle/worker/run-job.mjs +166 -274
- package/extensions/oracle/worker/state-locks.mjs +31 -216
- package/package.json +4 -3
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface OracleAuthLoginProbe {
|
|
2
|
+
ok: boolean;
|
|
3
|
+
status: number;
|
|
4
|
+
pageUrl?: string;
|
|
5
|
+
domLoginCta?: boolean;
|
|
6
|
+
onAuthPage?: boolean;
|
|
7
|
+
error?: string;
|
|
8
|
+
bodyKeys?: string[];
|
|
9
|
+
bodyHasId?: boolean;
|
|
10
|
+
bodyHasEmail?: boolean;
|
|
11
|
+
name?: string;
|
|
12
|
+
responsePreview?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type OracleAuthPageState =
|
|
16
|
+
| "challenge_blocking"
|
|
17
|
+
| "login_required"
|
|
18
|
+
| "transient_outage_error"
|
|
19
|
+
| "auth_transitioning"
|
|
20
|
+
| "authenticated_and_ready"
|
|
21
|
+
| "unknown";
|
|
22
|
+
|
|
23
|
+
export interface OracleAuthPageClassification {
|
|
24
|
+
state: OracleAuthPageState;
|
|
25
|
+
message: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export declare function normalizeLoginProbeResult(result: unknown): OracleAuthLoginProbe;
|
|
29
|
+
export declare function buildAccountChooserCandidateLabels(name?: string): string[];
|
|
30
|
+
export declare function classifyChatAuthPage(args: {
|
|
31
|
+
url: string;
|
|
32
|
+
snapshot: string;
|
|
33
|
+
body: string;
|
|
34
|
+
probe?: OracleAuthLoginProbe;
|
|
35
|
+
allowedOrigins: readonly string[];
|
|
36
|
+
cookieSourceLabel: string;
|
|
37
|
+
runtimeProfileDir: string;
|
|
38
|
+
logPath: string;
|
|
39
|
+
composerLabel?: string;
|
|
40
|
+
addFilesLabel?: string;
|
|
41
|
+
}): OracleAuthPageClassification;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// Purpose: Provide pure auth-bootstrap classification helpers for ChatGPT browser state handling.
|
|
2
|
+
// Responsibilities: Normalize login-probe payloads, classify ChatGPT auth page states, and derive account chooser candidate labels.
|
|
3
|
+
// Scope: Pure worker/auth decision logic only; browser I/O, logging, and side effects stay in auth-bootstrap.mjs.
|
|
4
|
+
// Usage: Imported by auth-bootstrap.mjs and sanity tests to exercise auth classification behavior without driving a browser.
|
|
5
|
+
// Invariants/Assumptions: Inputs are already captured snapshots/probe results from the live browser session; outputs are deterministic and side-effect free.
|
|
6
|
+
|
|
7
|
+
/** @typedef {import("./auth-flow-helpers.d.mts").OracleAuthLoginProbe} OracleAuthLoginProbe */
|
|
8
|
+
/** @typedef {import("./auth-flow-helpers.d.mts").OracleAuthPageClassification} OracleAuthPageClassification */
|
|
9
|
+
|
|
10
|
+
const DEFAULT_COMPOSER_LABEL = "Chat with ChatGPT";
|
|
11
|
+
const DEFAULT_ADD_FILES_LABEL = "Add files and more";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {unknown} result
|
|
15
|
+
* @returns {OracleAuthLoginProbe}
|
|
16
|
+
*/
|
|
17
|
+
export function normalizeLoginProbeResult(result) {
|
|
18
|
+
if (!result || typeof result !== "object") {
|
|
19
|
+
return { ok: false, status: 0, error: "invalid-probe-result" };
|
|
20
|
+
}
|
|
21
|
+
const value = /** @type {Record<string, unknown>} */ (result);
|
|
22
|
+
return {
|
|
23
|
+
ok: value.ok === true,
|
|
24
|
+
status: typeof value.status === "number" ? value.status : 0,
|
|
25
|
+
pageUrl: typeof value.pageUrl === "string" ? value.pageUrl : undefined,
|
|
26
|
+
domLoginCta: value.domLoginCta === true,
|
|
27
|
+
onAuthPage: value.onAuthPage === true,
|
|
28
|
+
error: typeof value.error === "string" ? value.error : undefined,
|
|
29
|
+
bodyKeys: Array.isArray(value.bodyKeys) ? value.bodyKeys.filter((entry) => typeof entry === "string") : [],
|
|
30
|
+
bodyHasId: value.bodyHasId === true,
|
|
31
|
+
bodyHasEmail: value.bodyHasEmail === true,
|
|
32
|
+
name: typeof value.name === "string" ? value.name : undefined,
|
|
33
|
+
responsePreview: typeof value.responsePreview === "string" ? value.responsePreview : undefined,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {string | undefined} name
|
|
39
|
+
* @returns {string[]}
|
|
40
|
+
*/
|
|
41
|
+
export function buildAccountChooserCandidateLabels(name) {
|
|
42
|
+
const normalized = typeof name === "string" ? name.trim() : "";
|
|
43
|
+
if (!normalized) return [];
|
|
44
|
+
const firstToken = normalized.split(/\s+/)[0] || "";
|
|
45
|
+
return firstToken && firstToken !== normalized ? [normalized, firstToken] : [normalized];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {{
|
|
50
|
+
* url: string;
|
|
51
|
+
* snapshot: string;
|
|
52
|
+
* body: string;
|
|
53
|
+
* probe?: OracleAuthLoginProbe;
|
|
54
|
+
* allowedOrigins: readonly string[];
|
|
55
|
+
* cookieSourceLabel: string;
|
|
56
|
+
* runtimeProfileDir: string;
|
|
57
|
+
* logPath: string;
|
|
58
|
+
* composerLabel?: string;
|
|
59
|
+
* addFilesLabel?: string;
|
|
60
|
+
* }} args
|
|
61
|
+
* @returns {OracleAuthPageClassification}
|
|
62
|
+
*/
|
|
63
|
+
export function classifyChatAuthPage(args) {
|
|
64
|
+
const text = `${args.snapshot}\n${args.body}`;
|
|
65
|
+
const composerLabel = args.composerLabel || DEFAULT_COMPOSER_LABEL;
|
|
66
|
+
const addFilesLabel = args.addFilesLabel || DEFAULT_ADD_FILES_LABEL;
|
|
67
|
+
const onAllowedOrigin = args.allowedOrigins.some((origin) => args.url.startsWith(origin));
|
|
68
|
+
const hasComposer = args.snapshot.includes(`textbox "${composerLabel}"`);
|
|
69
|
+
const hasAddFiles = args.snapshot.includes(`button "${addFilesLabel}"`);
|
|
70
|
+
const hasModelControl =
|
|
71
|
+
args.snapshot.includes('button "Model selector"') ||
|
|
72
|
+
/button "(Instant|Thinking|Pro)(?: [^"]*)?"/.test(args.snapshot);
|
|
73
|
+
|
|
74
|
+
const challengePatterns = [
|
|
75
|
+
/just a moment/i,
|
|
76
|
+
/verify you are human/i,
|
|
77
|
+
/cloudflare/i,
|
|
78
|
+
/captcha|turnstile|hcaptcha/i,
|
|
79
|
+
/unusual activity detected/i,
|
|
80
|
+
/we detect suspicious activity/i,
|
|
81
|
+
];
|
|
82
|
+
if (challengePatterns.some((pattern) => pattern.test(text))) {
|
|
83
|
+
return {
|
|
84
|
+
state: "challenge_blocking",
|
|
85
|
+
message:
|
|
86
|
+
`ChatGPT challenge detected after syncing cookies from ${args.cookieSourceLabel}. ` +
|
|
87
|
+
`The isolated oracle browser was left open on profile ${args.runtimeProfileDir}; complete the challenge there, then rerun /oracle-auth. Logs: ${args.logPath}`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (/http error 431|request header or cookie too large/i.test(text)) {
|
|
92
|
+
return {
|
|
93
|
+
state: "login_required",
|
|
94
|
+
message:
|
|
95
|
+
`Imported auth hit HTTP 431 during ChatGPT auth resolution, which usually means the imported cookie set is too large or stale. ` +
|
|
96
|
+
`Inspect ${args.logPath}.`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const outagePatterns = [
|
|
101
|
+
/something went wrong/i,
|
|
102
|
+
/a network error occurred/i,
|
|
103
|
+
/an error occurred while connecting to the websocket/i,
|
|
104
|
+
/try again later/i,
|
|
105
|
+
];
|
|
106
|
+
if (outagePatterns.some((pattern) => pattern.test(text))) {
|
|
107
|
+
return { state: "transient_outage_error", message: `ChatGPT is showing a transient outage/error page. Logs: ${args.logPath}` };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (args.probe?.status === 401 || args.probe?.status === 403) {
|
|
111
|
+
return {
|
|
112
|
+
state: "login_required",
|
|
113
|
+
message:
|
|
114
|
+
`Synced cookies from ${args.cookieSourceLabel}, but ChatGPT still rejected the session ` +
|
|
115
|
+
`(status=${args.probe?.status ?? 0}). Check auth.chromeProfile/auth.chromeCookiePath and inspect ${args.logPath}.`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (args.probe?.onAuthPage) {
|
|
120
|
+
if (args.probe?.bodyHasId || args.probe?.bodyHasEmail) {
|
|
121
|
+
return {
|
|
122
|
+
state: "auth_transitioning",
|
|
123
|
+
message:
|
|
124
|
+
`ChatGPT is on /auth/login, but /backend-api/me returned a partial authenticated session. ` +
|
|
125
|
+
`Trying to drive the login resolution flow. Logs: ${args.logPath}`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
state: "login_required",
|
|
130
|
+
message:
|
|
131
|
+
`Synced cookies from ${args.cookieSourceLabel}, but ChatGPT still rejected the session ` +
|
|
132
|
+
`(status=${args.probe?.status ?? 0}). Check auth.chromeProfile/auth.chromeCookiePath and inspect ${args.logPath}.`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (onAllowedOrigin && args.probe?.status === 200 && hasComposer && hasAddFiles && hasModelControl) {
|
|
137
|
+
if (!args.probe?.domLoginCta) {
|
|
138
|
+
return {
|
|
139
|
+
state: "authenticated_and_ready",
|
|
140
|
+
message: `Imported ChatGPT auth from ${args.cookieSourceLabel} into the isolated oracle profile. Logs: ${args.logPath}`,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
state: "auth_transitioning",
|
|
146
|
+
message:
|
|
147
|
+
args.probe?.bodyHasId || args.probe?.bodyHasEmail
|
|
148
|
+
? `ChatGPT backend session is authenticated but the shell still shows public CTA chrome. Logs: ${args.logPath}`
|
|
149
|
+
: `ChatGPT accepted cookies but is still hydrating/auth-selecting. Logs: ${args.logPath}`,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (onAllowedOrigin && args.probe?.ok && hasComposer && hasAddFiles && hasModelControl) {
|
|
154
|
+
return {
|
|
155
|
+
state: "authenticated_and_ready",
|
|
156
|
+
message: `Imported ChatGPT auth from ${args.cookieSourceLabel} into the isolated oracle profile. Logs: ${args.logPath}`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (args.url && !onAllowedOrigin) {
|
|
161
|
+
return { state: "login_required", message: `Imported auth redirected away from the expected ChatGPT origin. Logs: ${args.logPath}` };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return { state: "unknown", message: `ChatGPT page state is not yet ready. Logs: ${args.logPath}` };
|
|
165
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface OracleStableValueState {
|
|
2
|
+
lastValue: string;
|
|
3
|
+
stableCount: number;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export declare function assistantSnapshotSlice(snapshot: string, composerLabel: string, responseIndex: number): string | undefined;
|
|
7
|
+
export declare function stripUrlQueryAndHash(url: string | undefined): string;
|
|
8
|
+
export declare function isConversationPathUrl(url: string): boolean;
|
|
9
|
+
export declare function resolveStableConversationUrlCandidate(url: string, previousChatUrl?: string): string | undefined;
|
|
10
|
+
export declare function nextStableValueState(
|
|
11
|
+
state: Partial<OracleStableValueState> | undefined,
|
|
12
|
+
nextValue: string,
|
|
13
|
+
): OracleStableValueState;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Purpose: Provide pure ChatGPT conversation-state helpers used by the oracle worker.
|
|
2
|
+
// Responsibilities: Slice assistant snapshot regions, normalize URLs, and track stable conversation URL observations.
|
|
3
|
+
// Scope: Pure worker flow logic only; browser I/O and polling loops stay in run-job.mjs.
|
|
4
|
+
// Usage: Imported by run-job.mjs and sanity tests to validate conversation-state heuristics without driving a browser.
|
|
5
|
+
// Invariants/Assumptions: Snapshot text comes from agent-browser `snapshot -i`; URL inputs may be malformed and must fail safely.
|
|
6
|
+
|
|
7
|
+
/** @typedef {import("./chatgpt-flow-helpers.d.mts").OracleStableValueState} OracleStableValueState */
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} snapshot
|
|
11
|
+
* @param {string} composerLabel
|
|
12
|
+
* @param {number} responseIndex
|
|
13
|
+
* @returns {string | undefined}
|
|
14
|
+
*/
|
|
15
|
+
export function assistantSnapshotSlice(snapshot, composerLabel, responseIndex) {
|
|
16
|
+
const lines = snapshot.split("\n");
|
|
17
|
+
const assistantHeadingIndices = lines.flatMap((line, index) => (line.includes('heading "ChatGPT said:"') ? [index] : []));
|
|
18
|
+
const startIndex = assistantHeadingIndices[responseIndex];
|
|
19
|
+
if (startIndex === undefined) return undefined;
|
|
20
|
+
|
|
21
|
+
const endCandidates = [];
|
|
22
|
+
const nextAssistantIndex = assistantHeadingIndices[responseIndex + 1];
|
|
23
|
+
if (nextAssistantIndex !== undefined) endCandidates.push(nextAssistantIndex);
|
|
24
|
+
|
|
25
|
+
const composerIndex = lines.findIndex(
|
|
26
|
+
(line, index) => index > startIndex && line.includes(`textbox "${composerLabel}"`),
|
|
27
|
+
);
|
|
28
|
+
if (composerIndex !== -1) endCandidates.push(composerIndex);
|
|
29
|
+
|
|
30
|
+
const endIndex = endCandidates.length > 0 ? Math.min(...endCandidates) : undefined;
|
|
31
|
+
return lines.slice(startIndex, endIndex).join("\n");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {string | undefined} url
|
|
36
|
+
* @returns {string}
|
|
37
|
+
*/
|
|
38
|
+
export function stripUrlQueryAndHash(url) {
|
|
39
|
+
if (typeof url !== "string") return "";
|
|
40
|
+
try {
|
|
41
|
+
const parsed = new URL(url);
|
|
42
|
+
parsed.hash = "";
|
|
43
|
+
parsed.search = "";
|
|
44
|
+
return parsed.toString();
|
|
45
|
+
} catch {
|
|
46
|
+
return url;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {string} url
|
|
52
|
+
* @returns {boolean}
|
|
53
|
+
*/
|
|
54
|
+
export function isConversationPathUrl(url) {
|
|
55
|
+
try {
|
|
56
|
+
return /\/c\/[A-Za-z0-9-]+$/i.test(new URL(url).pathname);
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {string} url
|
|
64
|
+
* @param {string | undefined} previousChatUrl
|
|
65
|
+
* @returns {string | undefined}
|
|
66
|
+
*/
|
|
67
|
+
export function resolveStableConversationUrlCandidate(url, previousChatUrl) {
|
|
68
|
+
const normalizedUrl = stripUrlQueryAndHash(url);
|
|
69
|
+
if (!normalizedUrl) return undefined;
|
|
70
|
+
if (isConversationPathUrl(normalizedUrl)) return normalizedUrl;
|
|
71
|
+
const normalizedPrevious = stripUrlQueryAndHash(previousChatUrl);
|
|
72
|
+
return normalizedPrevious && normalizedPrevious === normalizedUrl ? normalizedUrl : undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @param {Partial<OracleStableValueState> | undefined} state
|
|
77
|
+
* @param {string} nextValue
|
|
78
|
+
* @returns {OracleStableValueState}
|
|
79
|
+
*/
|
|
80
|
+
export function nextStableValueState(state, nextValue) {
|
|
81
|
+
return {
|
|
82
|
+
lastValue: nextValue,
|
|
83
|
+
stableCount: state?.lastValue === nextValue ? (state?.stableCount ?? 0) + 1 : 1,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -1,10 +1,24 @@
|
|
|
1
|
+
// Purpose: Provide pure ChatGPT UI interpretation helpers shared by oracle worker/auth flows.
|
|
2
|
+
// Responsibilities: Normalize allowed origins, interpret model-selection snapshots, and derive assistant-completion signatures.
|
|
3
|
+
// Scope: Pure snapshot/text heuristics only; browser I/O and retry loops stay in the worker/auth entrypoints.
|
|
4
|
+
// Usage: Imported by worker/auth runtime code and sanity tests to keep browser-driven logic behaviorally testable.
|
|
5
|
+
// Invariants/Assumptions: Snapshot text comes from agent-browser `snapshot -i`; helper outputs must stay deterministic and side-effect free.
|
|
6
|
+
|
|
1
7
|
import { parseSnapshotEntries } from "./artifact-heuristics.mjs";
|
|
2
8
|
|
|
9
|
+
/** @typedef {import("./chatgpt-ui-helpers.d.mts").OracleUiModelFamily} OracleUiModelFamily */
|
|
10
|
+
/** @typedef {import("./chatgpt-ui-helpers.d.mts").OracleUiSelection} OracleUiSelection */
|
|
11
|
+
/** @typedef {import("./artifact-heuristics.d.mts").SnapshotEntry} SnapshotEntry */
|
|
12
|
+
|
|
13
|
+
/** @typedef {{ responseText: string; artifactLabels?: string[]; suspiciousArtifactLabels?: string[] }} CompletionSignatureArgs */
|
|
14
|
+
/** @typedef {{ hasStopStreaming: boolean; hasTargetCopyResponse: boolean; responseText: string; artifactLabels?: string[]; suspiciousArtifactLabels?: string[] }} DerivedCompletionSignatureArgs */
|
|
15
|
+
|
|
3
16
|
export const CHATGPT_CANONICAL_APP_ORIGINS = Object.freeze([
|
|
4
17
|
"https://chatgpt.com",
|
|
5
18
|
"https://chat.openai.com",
|
|
6
19
|
]);
|
|
7
20
|
|
|
21
|
+
/** @type {Record<OracleUiModelFamily, string>} */
|
|
8
22
|
const MODEL_FAMILY_PREFIX = {
|
|
9
23
|
instant: "Instant ",
|
|
10
24
|
thinking: "Thinking ",
|
|
@@ -13,6 +27,10 @@ const MODEL_FAMILY_PREFIX = {
|
|
|
13
27
|
|
|
14
28
|
const AUTO_SWITCH_LABEL = "Auto-switch to Thinking";
|
|
15
29
|
|
|
30
|
+
/**
|
|
31
|
+
* @param {string | undefined} url
|
|
32
|
+
* @returns {string | undefined}
|
|
33
|
+
*/
|
|
16
34
|
function originFromUrl(url) {
|
|
17
35
|
if (typeof url !== "string" || !url.trim()) return undefined;
|
|
18
36
|
try {
|
|
@@ -22,18 +40,35 @@ function originFromUrl(url) {
|
|
|
22
40
|
}
|
|
23
41
|
}
|
|
24
42
|
|
|
43
|
+
/**
|
|
44
|
+
* @param {Array<string | undefined>} values
|
|
45
|
+
* @returns {string[]}
|
|
46
|
+
*/
|
|
25
47
|
function uniqueStrings(values) {
|
|
26
48
|
return [...new Set(values.filter((value) => typeof value === "string" && value))];
|
|
27
49
|
}
|
|
28
50
|
|
|
51
|
+
/**
|
|
52
|
+
* @param {string | undefined} value
|
|
53
|
+
* @returns {string | undefined}
|
|
54
|
+
*/
|
|
29
55
|
function titleCase(value) {
|
|
30
56
|
return value ? `${value[0].toUpperCase()}${value.slice(1)}` : value;
|
|
31
57
|
}
|
|
32
58
|
|
|
59
|
+
/**
|
|
60
|
+
* @param {string | undefined} value
|
|
61
|
+
* @returns {string}
|
|
62
|
+
*/
|
|
33
63
|
function normalizeText(value) {
|
|
34
64
|
return String(value || "").replace(/\s+/g, " ").trim();
|
|
35
65
|
}
|
|
36
66
|
|
|
67
|
+
/**
|
|
68
|
+
* @param {string} chatUrl
|
|
69
|
+
* @param {string | undefined} authUrl
|
|
70
|
+
* @returns {string[]}
|
|
71
|
+
*/
|
|
37
72
|
export function buildAllowedChatGptOrigins(chatUrl, authUrl) {
|
|
38
73
|
return uniqueStrings([
|
|
39
74
|
...CHATGPT_CANONICAL_APP_ORIGINS,
|
|
@@ -43,6 +78,11 @@ export function buildAllowedChatGptOrigins(chatUrl, authUrl) {
|
|
|
43
78
|
]);
|
|
44
79
|
}
|
|
45
80
|
|
|
81
|
+
/**
|
|
82
|
+
* @param {string | undefined} label
|
|
83
|
+
* @param {OracleUiModelFamily} family
|
|
84
|
+
* @returns {boolean}
|
|
85
|
+
*/
|
|
46
86
|
export function matchesModelFamilyLabel(label, family) {
|
|
47
87
|
const normalized = String(label || "");
|
|
48
88
|
const prefix = MODEL_FAMILY_PREFIX[family];
|
|
@@ -50,12 +90,22 @@ export function matchesModelFamilyLabel(label, family) {
|
|
|
50
90
|
return normalized === exact || normalized.startsWith(prefix) || normalized.startsWith(`${exact},`);
|
|
51
91
|
}
|
|
52
92
|
|
|
93
|
+
/**
|
|
94
|
+
* @param {OracleUiSelection} selection
|
|
95
|
+
* @returns {string | undefined}
|
|
96
|
+
*/
|
|
53
97
|
export function requestedEffortLabel(selection) {
|
|
54
98
|
return selection?.effort ? titleCase(selection.effort) : undefined;
|
|
55
99
|
}
|
|
56
100
|
|
|
101
|
+
/**
|
|
102
|
+
* @param {string} snapshot
|
|
103
|
+
* @param {string | undefined} effortLabel
|
|
104
|
+
* @returns {boolean}
|
|
105
|
+
*/
|
|
57
106
|
export function effortSelectionVisible(snapshot, effortLabel) {
|
|
58
107
|
if (!effortLabel) return true;
|
|
108
|
+
/** @type {SnapshotEntry[]} */
|
|
59
109
|
const entries = parseSnapshotEntries(snapshot);
|
|
60
110
|
return entries.some((entry) => {
|
|
61
111
|
if (entry.disabled) return false;
|
|
@@ -72,19 +122,27 @@ export function effortSelectionVisible(snapshot, effortLabel) {
|
|
|
72
122
|
});
|
|
73
123
|
}
|
|
74
124
|
|
|
125
|
+
/**
|
|
126
|
+
* @param {string} snapshot
|
|
127
|
+
* @returns {boolean}
|
|
128
|
+
*/
|
|
75
129
|
export function thinkingChipVisible(snapshot) {
|
|
76
130
|
return /button "(?:Light|Standard|Extended|Heavy)(?: thinking)?(?:, click to remove)?"/i.test(snapshot);
|
|
77
131
|
}
|
|
78
132
|
|
|
133
|
+
/**
|
|
134
|
+
* @param {string} snapshot
|
|
135
|
+
* @returns {boolean}
|
|
136
|
+
*/
|
|
79
137
|
export function snapshotHasModelConfigurationUi(snapshot) {
|
|
138
|
+
/** @type {SnapshotEntry[]} */
|
|
80
139
|
const entries = parseSnapshotEntries(snapshot);
|
|
81
140
|
const visibleFamilies = new Set(
|
|
82
141
|
entries
|
|
83
142
|
.filter((entry) => entry.kind === "button" && typeof entry.label === "string")
|
|
84
143
|
.flatMap((entry) =>
|
|
85
|
-
|
|
86
|
-
.filter((family) => matchesModelFamilyLabel(entry.label, family))
|
|
87
|
-
.map((family) => family),
|
|
144
|
+
/** @type {OracleUiModelFamily[]} */ (["instant", "thinking", "pro"])
|
|
145
|
+
.filter((family) => matchesModelFamilyLabel(entry.label, family)),
|
|
88
146
|
),
|
|
89
147
|
);
|
|
90
148
|
const hasCloseButton = entries.some((entry) => entry.kind === "button" && entry.label === "Close" && !entry.disabled);
|
|
@@ -94,7 +152,12 @@ export function snapshotHasModelConfigurationUi(snapshot) {
|
|
|
94
152
|
return visibleFamilies.size >= 2 || hasCloseButton || hasEffortCombobox;
|
|
95
153
|
}
|
|
96
154
|
|
|
155
|
+
/**
|
|
156
|
+
* @param {string} snapshot
|
|
157
|
+
* @returns {boolean | undefined}
|
|
158
|
+
*/
|
|
97
159
|
export function autoSwitchToThinkingSelectionVisible(snapshot) {
|
|
160
|
+
/** @type {SnapshotEntry[]} */
|
|
98
161
|
const entries = parseSnapshotEntries(snapshot);
|
|
99
162
|
let foundControl = false;
|
|
100
163
|
|
|
@@ -111,6 +174,11 @@ export function autoSwitchToThinkingSelectionVisible(snapshot) {
|
|
|
111
174
|
return foundControl ? false : undefined;
|
|
112
175
|
}
|
|
113
176
|
|
|
177
|
+
/**
|
|
178
|
+
* @param {string} snapshot
|
|
179
|
+
* @param {OracleUiSelection} selection
|
|
180
|
+
* @returns {boolean}
|
|
181
|
+
*/
|
|
114
182
|
export function snapshotCanSafelySkipModelConfiguration(snapshot, selection) {
|
|
115
183
|
if (!snapshotStronglyMatchesRequestedModel(snapshot, selection)) return false;
|
|
116
184
|
|
|
@@ -126,11 +194,15 @@ export function snapshotCanSafelySkipModelConfiguration(snapshot, selection) {
|
|
|
126
194
|
return true;
|
|
127
195
|
}
|
|
128
196
|
|
|
197
|
+
/**
|
|
198
|
+
* @param {string} snapshot
|
|
199
|
+
* @param {OracleUiSelection} selection
|
|
200
|
+
* @returns {boolean}
|
|
201
|
+
*/
|
|
129
202
|
export function snapshotStronglyMatchesRequestedModel(snapshot, selection) {
|
|
203
|
+
/** @type {SnapshotEntry[]} */
|
|
130
204
|
const entries = parseSnapshotEntries(snapshot);
|
|
131
|
-
const familyMatched = entries.some((entry) =>
|
|
132
|
-
return !entry.disabled && matchesModelFamilyLabel(entry.label, selection.modelFamily);
|
|
133
|
-
});
|
|
205
|
+
const familyMatched = entries.some((entry) => !entry.disabled && matchesModelFamilyLabel(entry.label, selection.modelFamily));
|
|
134
206
|
if (!familyMatched) return false;
|
|
135
207
|
|
|
136
208
|
const configurationUiVisible = snapshotHasModelConfigurationUi(snapshot);
|
|
@@ -153,11 +225,15 @@ export function snapshotStronglyMatchesRequestedModel(snapshot, selection) {
|
|
|
153
225
|
return false;
|
|
154
226
|
}
|
|
155
227
|
|
|
228
|
+
/**
|
|
229
|
+
* @param {string} snapshot
|
|
230
|
+
* @param {OracleUiSelection} selection
|
|
231
|
+
* @returns {boolean}
|
|
232
|
+
*/
|
|
156
233
|
export function snapshotWeaklyMatchesRequestedModel(snapshot, selection) {
|
|
234
|
+
/** @type {SnapshotEntry[]} */
|
|
157
235
|
const entries = parseSnapshotEntries(snapshot);
|
|
158
|
-
const familyMatched = entries.some((entry) =>
|
|
159
|
-
return !entry.disabled && matchesModelFamilyLabel(entry.label, selection.modelFamily);
|
|
160
|
-
});
|
|
236
|
+
const familyMatched = entries.some((entry) => !entry.disabled && matchesModelFamilyLabel(entry.label, selection.modelFamily));
|
|
161
237
|
|
|
162
238
|
if (selection.modelFamily === "thinking") {
|
|
163
239
|
return familyMatched || effortSelectionVisible(snapshot, requestedEffortLabel(selection));
|
|
@@ -177,6 +253,10 @@ export function snapshotWeaklyMatchesRequestedModel(snapshot, selection) {
|
|
|
177
253
|
return false;
|
|
178
254
|
}
|
|
179
255
|
|
|
256
|
+
/**
|
|
257
|
+
* @param {CompletionSignatureArgs} args
|
|
258
|
+
* @returns {string | undefined}
|
|
259
|
+
*/
|
|
180
260
|
export function buildAssistantCompletionSignature({ responseText, artifactLabels = [], suspiciousArtifactLabels = [] }) {
|
|
181
261
|
const normalizedResponse = normalizeText(responseText);
|
|
182
262
|
if (normalizedResponse) return `text:${normalizedResponse}`;
|
|
@@ -187,6 +267,10 @@ export function buildAssistantCompletionSignature({ responseText, artifactLabels
|
|
|
187
267
|
return undefined;
|
|
188
268
|
}
|
|
189
269
|
|
|
270
|
+
/**
|
|
271
|
+
* @param {DerivedCompletionSignatureArgs} args
|
|
272
|
+
* @returns {string | undefined}
|
|
273
|
+
*/
|
|
190
274
|
export function deriveAssistantCompletionSignature({
|
|
191
275
|
hasStopStreaming,
|
|
192
276
|
hasTargetCopyResponse,
|