@tomflow/proflow-execution-browser-extension 0.1.11 → 0.1.13

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.
@@ -0,0 +1,1033 @@
1
+ import {
2
+ type CustomGptCapability,
3
+ type CustomGptEditorPort,
4
+ type CustomGptKnowledgeFile,
5
+ type CustomGptProvisioningRequest,
6
+ createCustomGptEditorDriver,
7
+ parseCustomGptProvisioningRequest,
8
+ } from "../src/custom-gpt-editor-driver.js";
9
+
10
+ type ProvisioningSurface = {
11
+ kind: "CUSTOM_GPT_DEPLOYMENT_PROVISIONING";
12
+ instanceId: string;
13
+ url: string;
14
+ };
15
+
16
+ type ProvisioningCommand = {
17
+ type: "PROFLOW_PROVISIONING_COMMAND";
18
+ operation: "PROVISION_CUSTOM_GPT" | "FINALIZE_CUSTOM_GPT_AUTH";
19
+ request: Record<string, unknown>;
20
+ };
21
+
22
+ type ChromeRuntime = {
23
+ runtime: {
24
+ sendMessage(message: unknown): Promise<unknown>;
25
+ onMessage: {
26
+ addListener(
27
+ listener: (
28
+ message: ProvisioningCommand,
29
+ sender: unknown,
30
+ sendResponse: (value: unknown) => void,
31
+ ) => boolean | undefined,
32
+ ): void;
33
+ };
34
+ };
35
+ };
36
+ declare const chrome: ChromeRuntime;
37
+
38
+ const provisioningSurface: ProvisioningSurface = Object.freeze({
39
+ kind: "CUSTOM_GPT_DEPLOYMENT_PROVISIONING",
40
+ instanceId: `provisioning:${crypto.randomUUID()}`,
41
+ url: location.href,
42
+ });
43
+
44
+ const sleep = (milliseconds: number) =>
45
+ new Promise<void>((resolve) => setTimeout(resolve, milliseconds));
46
+
47
+ function normalize(value: string | null | undefined): string {
48
+ return (value ?? "").replace(/\s+/g, " ").trim().toLowerCase();
49
+ }
50
+
51
+ function editorSurfaceReady(): boolean {
52
+ return (
53
+ location.protocol === "https:" &&
54
+ location.hostname === "chatgpt.com" &&
55
+ location.pathname.startsWith("/gpts/editor")
56
+ );
57
+ }
58
+
59
+ function elementSemanticText(element: Element): string {
60
+ return normalize(
61
+ [
62
+ element.getAttribute("aria-label"),
63
+ element.getAttribute("placeholder"),
64
+ element.getAttribute("name"),
65
+ element.textContent,
66
+ ]
67
+ .filter(Boolean)
68
+ .join(" "),
69
+ );
70
+ }
71
+
72
+ function matchesAny(element: Element, candidates: readonly string[]): boolean {
73
+ const semantic = elementSemanticText(element);
74
+ return candidates.some((candidate) =>
75
+ semantic.includes(normalize(candidate)),
76
+ );
77
+ }
78
+
79
+ function controlFromLabel(label: HTMLLabelElement): HTMLElement | null {
80
+ if (label.htmlFor) {
81
+ const linked = document.getElementById(label.htmlFor);
82
+ if (linked instanceof HTMLElement) return linked;
83
+ }
84
+ const selector =
85
+ 'input:not([type="file"]), textarea, select, [contenteditable="true"], [role="textbox"], [role="combobox"]';
86
+ const nested = label.querySelector<HTMLElement>(selector);
87
+ if (nested) return nested;
88
+ let scope: HTMLElement | null = label.parentElement;
89
+ for (let depth = 0; depth < 4 && scope; depth += 1) {
90
+ const controls = [...scope.querySelectorAll<HTMLElement>(selector)];
91
+ if (controls.length === 1) return controls[0] ?? null;
92
+ scope = scope.parentElement;
93
+ }
94
+ return null;
95
+ }
96
+
97
+ function findControl(candidates: readonly string[]): HTMLElement {
98
+ for (const label of document.querySelectorAll("label"))
99
+ if (matchesAny(label, candidates)) {
100
+ const control = controlFromLabel(label);
101
+ if (control) return control;
102
+ }
103
+ for (const element of document.querySelectorAll<HTMLElement>(
104
+ 'input, textarea, select, [contenteditable="true"], [role="textbox"], [role="combobox"]',
105
+ ))
106
+ if (matchesAny(element, candidates)) return element;
107
+ throw new Error(`GPT_EDITOR_CONTROL_NOT_FOUND:${candidates[0] ?? "unknown"}`);
108
+ }
109
+
110
+ function setControlValue(element: HTMLElement, value: string): void {
111
+ if (
112
+ element instanceof HTMLInputElement ||
113
+ element instanceof HTMLTextAreaElement ||
114
+ element instanceof HTMLSelectElement
115
+ ) {
116
+ const prototype =
117
+ element instanceof HTMLInputElement
118
+ ? HTMLInputElement.prototype
119
+ : element instanceof HTMLTextAreaElement
120
+ ? HTMLTextAreaElement.prototype
121
+ : HTMLSelectElement.prototype;
122
+ const setter = Object.getOwnPropertyDescriptor(prototype, "value")?.set;
123
+ if (setter) setter.call(element, value);
124
+ else element.value = value;
125
+ } else {
126
+ element.textContent = value;
127
+ }
128
+ element.dispatchEvent(
129
+ new InputEvent("input", { bubbles: true, data: value }),
130
+ );
131
+ element.dispatchEvent(new Event("change", { bubbles: true }));
132
+ }
133
+
134
+ function controlValue(element: HTMLElement): string {
135
+ if (
136
+ element instanceof HTMLInputElement ||
137
+ element instanceof HTMLTextAreaElement ||
138
+ element instanceof HTMLSelectElement
139
+ )
140
+ return element.value.replaceAll("\r\n", "\n");
141
+ return (element.textContent ?? "").replaceAll("\r\n", "\n");
142
+ }
143
+
144
+ async function waitForReadback(
145
+ code: string,
146
+ predicate: () => boolean,
147
+ ): Promise<void> {
148
+ for (let attempt = 0; attempt < 600; attempt += 1) {
149
+ if (predicate()) return;
150
+ await sleep(100);
151
+ }
152
+ throw new Error(code);
153
+ }
154
+
155
+ function clickable(candidates: readonly string[]): HTMLElement | null {
156
+ for (const element of document.querySelectorAll<HTMLElement>(
157
+ 'button, [role="button"], [role="option"], [role="menuitem"], [role="radio"]',
158
+ ))
159
+ if (matchesAny(element, candidates)) return element;
160
+ return null;
161
+ }
162
+
163
+ async function waitForClickable(
164
+ candidates: readonly string[],
165
+ attempts = 30,
166
+ ): Promise<HTMLElement> {
167
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
168
+ const element = clickable(candidates);
169
+ if (element) return element;
170
+ await sleep(100);
171
+ }
172
+ throw new Error(`GPT_EDITOR_ACTION_NOT_FOUND:${candidates[0] ?? "unknown"}`);
173
+ }
174
+
175
+ const fieldLabels = {
176
+ displayName: ["Name", "名称", "Name your GPT", "为你的 GPT 命名"],
177
+ description: [
178
+ "Description",
179
+ "描述",
180
+ "Add a short description about what this GPT does",
181
+ "添加有关此 GPT 的功能的简短描述",
182
+ ],
183
+ instructions: ["Instructions", "指令"],
184
+ } as const;
185
+
186
+ async function ensureConfigureMode(): Promise<void> {
187
+ try {
188
+ findControl(fieldLabels.displayName);
189
+ return;
190
+ } catch {}
191
+ const configure = await waitForClickable(["Configure", "配置"], 60);
192
+ configure.click();
193
+ for (let attempt = 0; attempt < 80; attempt += 1) {
194
+ try {
195
+ findControl(fieldLabels.displayName);
196
+ return;
197
+ } catch {
198
+ await sleep(100);
199
+ }
200
+ }
201
+ throw new Error("GPT_EDITOR_CONFIGURE_SURFACE_NOT_READY");
202
+ }
203
+
204
+ const capabilityLabels: Record<CustomGptCapability, readonly string[]> = {
205
+ webSearch: ["Web search", "网页搜索", "网络搜索", "浏览网页"],
206
+ imageGeneration: ["Image generation", "图像生成", "图片生成"],
207
+ codeInterpreter: [
208
+ "Code Interpreter & Data Analysis",
209
+ "Code Interpreter",
210
+ "Data Analysis",
211
+ "代码解释器",
212
+ "代码解译器",
213
+ ],
214
+ };
215
+
216
+ function conversationStarterControls(minimum: number): HTMLElement[] {
217
+ const label = [...document.querySelectorAll<HTMLLabelElement>("label")].find(
218
+ (element) => matchesAny(element, ["Conversation starter", "对话开场白"]),
219
+ );
220
+ if (!label) return [];
221
+ let scope: HTMLElement | null = label.parentElement;
222
+ for (let depth = 0; depth < 4 && scope; depth += 1) {
223
+ const controls = [
224
+ ...scope.querySelectorAll<HTMLElement>(
225
+ 'input[type="text"], textarea:not([data-testid="gizmo-instructions-input"])',
226
+ ),
227
+ ];
228
+ if (controls.length >= minimum) return controls;
229
+ scope = scope.parentElement;
230
+ }
231
+ return [];
232
+ }
233
+
234
+ async function replaceConversationStarters(values: readonly string[]) {
235
+ const controls = conversationStarterControls(values.length);
236
+ if (controls.length < values.length)
237
+ throw new Error("GPT_EDITOR_STARTER_CONTROL_NOT_FOUND");
238
+ for (let index = 0; index < values.length; index += 1)
239
+ setControlValue(controls[index] as HTMLElement, values[index] ?? "");
240
+ }
241
+
242
+ function currentGptId(): string | null {
243
+ const match = /\/(g-[a-zA-Z0-9_-]+)(?:\/|$)/.exec(location.pathname);
244
+ return match?.[1] ?? null;
245
+ }
246
+
247
+ async function knowledgeFileInput(): Promise<HTMLInputElement> {
248
+ const label = [...document.querySelectorAll<HTMLLabelElement>("label")].find(
249
+ (element) => matchesAny(element, ["Knowledge", "知识"]),
250
+ );
251
+ if (label) {
252
+ let scope: HTMLElement | null = label.parentElement;
253
+ for (let depth = 0; depth < 4 && scope; depth += 1) {
254
+ const inputs = [
255
+ ...scope.querySelectorAll<HTMLInputElement>('input[type="file"]'),
256
+ ];
257
+ if (inputs.length === 1) return inputs[0] as HTMLInputElement;
258
+ scope = scope.parentElement;
259
+ }
260
+ }
261
+ const upload = clickable([
262
+ "Upload files",
263
+ "Upload file",
264
+ "Add files",
265
+ "上传文件",
266
+ "添加文件",
267
+ ]);
268
+ if (upload) upload.click();
269
+ for (let attempt = 0; attempt < 40; attempt += 1) {
270
+ const candidates = [
271
+ ...document.querySelectorAll<HTMLInputElement>('input[type="file"]'),
272
+ ];
273
+ if (candidates.length === 1) return candidates[0] as HTMLInputElement;
274
+ await sleep(100);
275
+ }
276
+ throw new Error("GPT_EDITOR_KNOWLEDGE_INPUT_NOT_FOUND");
277
+ }
278
+
279
+ async function fetchKnowledgeRelay(url: string): Promise<ArrayBuffer> {
280
+ const result = await chrome.runtime.sendMessage({
281
+ type: "PROFLOW_PROVISIONING_RELAY_FETCH",
282
+ url,
283
+ });
284
+ if (typeof result !== "object" || result === null)
285
+ throw new Error("KNOWLEDGE_RELAY_BACKGROUND_INVALID");
286
+ const record = result as Record<string, unknown>;
287
+ if (record.ok !== true || typeof record.base64 !== "string")
288
+ throw new Error(
289
+ typeof record.error === "string"
290
+ ? record.error
291
+ : "KNOWLEDGE_RELAY_BACKGROUND_FAILED",
292
+ );
293
+ const binary = atob(record.base64);
294
+ const bytes = new Uint8Array(binary.length);
295
+ for (let index = 0; index < binary.length; index += 1)
296
+ bytes[index] = binary.charCodeAt(index);
297
+ return bytes.buffer;
298
+ }
299
+
300
+ async function sha256Bytes(bytes: ArrayBuffer): Promise<string> {
301
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
302
+ return `sha256:${[...new Uint8Array(digest)]
303
+ .map((value) => value.toString(16).padStart(2, "0"))
304
+ .join("")}`;
305
+ }
306
+
307
+ async function waitForKnowledgeName(name: string): Promise<void> {
308
+ await waitForReadback(`GPT_EDITOR_KNOWLEDGE_UPLOAD_TIMEOUT:${name}`, () =>
309
+ knowledgeReadbackMatches(name),
310
+ );
311
+ }
312
+
313
+ async function uploadKnowledge(files: readonly CustomGptKnowledgeFile[]) {
314
+ for (const descriptor of files) {
315
+ const input = await knowledgeFileInput();
316
+ const bytes = await fetchKnowledgeRelay(descriptor.url);
317
+ if (bytes.byteLength !== descriptor.sizeBytes)
318
+ throw new Error("KNOWLEDGE_RELAY_SIZE_MISMATCH");
319
+ if ((await sha256Bytes(bytes)) !== descriptor.sha256)
320
+ throw new Error("KNOWLEDGE_RELAY_HASH_MISMATCH");
321
+ const transfer = new DataTransfer();
322
+ transfer.items.add(
323
+ new File([bytes], descriptor.name, { type: descriptor.mime }),
324
+ );
325
+ input.files = transfer.files;
326
+ input.dispatchEvent(new Event("change", { bubbles: true }));
327
+ await waitForKnowledgeName(descriptor.name);
328
+ }
329
+ }
330
+
331
+ const privateVisibilityLabels = [
332
+ "Only me",
333
+ "Private",
334
+ "只有我",
335
+ "私有",
336
+ "仅自己",
337
+ ] as const;
338
+
339
+ const nonPrivateVisibilityLabels = [
340
+ "Anyone",
341
+ "Public",
342
+ "Link",
343
+ "GPT Store",
344
+ "Workspace",
345
+ "所有人",
346
+ "公开",
347
+ "链接",
348
+ "工作区",
349
+ ] as const;
350
+
351
+ function semanticValues(element: Element): string[] {
352
+ const referencedText = (attribute: "aria-labelledby" | "aria-describedby") =>
353
+ (element.getAttribute(attribute) ?? "")
354
+ .split(/\s+/)
355
+ .filter(Boolean)
356
+ .map((id) => document.getElementById(id)?.textContent ?? "")
357
+ .join(" ");
358
+ return [
359
+ element.getAttribute("aria-label"),
360
+ referencedText("aria-labelledby"),
361
+ referencedText("aria-describedby"),
362
+ element.getAttribute("data-value"),
363
+ element.getAttribute("value"),
364
+ element.textContent,
365
+ ]
366
+ .filter((value): value is string => Boolean(value))
367
+ .map(normalize);
368
+ }
369
+
370
+ function matchesExactSemantic(
371
+ element: Element,
372
+ candidates: readonly string[],
373
+ ): boolean {
374
+ const expected = new Set(candidates.map(normalize));
375
+ return semanticValues(element).some((value) => expected.has(value));
376
+ }
377
+
378
+ function matchesBoundedSemantic(
379
+ element: Element,
380
+ candidates: readonly string[],
381
+ ): boolean {
382
+ const values = semanticValues(element);
383
+ return candidates.some((candidate) => {
384
+ const expected = normalize(candidate);
385
+ return values.some(
386
+ (value) =>
387
+ value === expected ||
388
+ value.startsWith(`${expected} `) ||
389
+ value.endsWith(` ${expected}`),
390
+ );
391
+ });
392
+ }
393
+
394
+ function available(element: HTMLElement): boolean {
395
+ const style = getComputedStyle(element);
396
+ return (
397
+ element.getClientRects().length > 0 &&
398
+ style.display !== "none" &&
399
+ style.visibility !== "hidden" &&
400
+ element.getAttribute("aria-hidden") !== "true" &&
401
+ !element.hasAttribute("disabled") &&
402
+ element.getAttribute("aria-disabled") !== "true"
403
+ );
404
+ }
405
+
406
+ function privateVisibilityControl(): HTMLElement | null {
407
+ const selector =
408
+ 'input[type="radio"], label, button, [role="button"], [role="radio"], [role="option"], [role="menuitem"], [role="menuitemradio"]';
409
+ for (const element of document.querySelectorAll<HTMLElement>(selector)) {
410
+ if (
411
+ available(element) &&
412
+ matchesBoundedSemantic(element, privateVisibilityLabels) &&
413
+ !matchesBoundedSemantic(element, nonPrivateVisibilityLabels)
414
+ )
415
+ return element;
416
+ }
417
+ return null;
418
+ }
419
+
420
+ function selected(element: HTMLElement): boolean {
421
+ if (element instanceof HTMLInputElement && element.type === "radio")
422
+ return element.checked;
423
+ if (element instanceof HTMLLabelElement && element.htmlFor) {
424
+ const control = document.getElementById(element.htmlFor);
425
+ if (control instanceof HTMLInputElement && control.type === "radio")
426
+ return control.checked;
427
+ }
428
+ return (
429
+ element.getAttribute("aria-checked") === "true" ||
430
+ element.getAttribute("aria-selected") === "true" ||
431
+ element.getAttribute("aria-pressed") === "true" ||
432
+ element.getAttribute("data-state") === "checked"
433
+ );
434
+ }
435
+
436
+ function carrierGptId(value: unknown): string {
437
+ if (typeof value !== "string") throw new Error("ROLE_CARRIER_URL_INVALID");
438
+ const url = new URL(value);
439
+ const match = /^\/g\/(g-[A-Za-z0-9_-]+)$/.exec(url.pathname);
440
+ if (
441
+ url.origin !== "https://chatgpt.com" ||
442
+ url.username !== "" ||
443
+ url.password !== "" ||
444
+ url.search !== "" ||
445
+ url.hash !== "" ||
446
+ !match?.[1]
447
+ )
448
+ throw new Error("ROLE_CARRIER_URL_INVALID");
449
+ return match[1];
450
+ }
451
+
452
+ async function waitForAuthSemantic(
453
+ root: ParentNode,
454
+ candidates: readonly string[],
455
+ selector = 'button, [role="button"], [role="radio"], [role="option"], label',
456
+ ): Promise<HTMLElement> {
457
+ for (let attempt = 0; attempt < 120; attempt += 1) {
458
+ for (const element of root.querySelectorAll<HTMLElement>(selector))
459
+ if (available(element) && matchesBoundedSemantic(element, candidates))
460
+ return element;
461
+ await sleep(100);
462
+ }
463
+ throw new Error(
464
+ `GPT_EDITOR_AUTH_CONTROL_NOT_FOUND:${candidates[0] ?? "unknown"}`,
465
+ );
466
+ }
467
+
468
+ function existingActionEditButton(): HTMLElement | null {
469
+ const actionLabel = [...document.querySelectorAll<HTMLElement>("label")].find(
470
+ (element) => matchesExactSemantic(element, ["Actions", "操作"]),
471
+ );
472
+ let scope: HTMLElement | null = actionLabel?.parentElement ?? null;
473
+ for (let depth = 0; depth < 5 && scope; depth += 1) {
474
+ const buttons = [
475
+ ...scope.querySelectorAll<HTMLElement>('button, [role="button"]'),
476
+ ].filter(available);
477
+ const create = buttons.find((element) =>
478
+ matchesBoundedSemantic(element, [
479
+ "Create new action",
480
+ "New action",
481
+ "创建新操作",
482
+ ]),
483
+ );
484
+ if (create) {
485
+ return (
486
+ buttons.find(
487
+ (element) =>
488
+ element !== create &&
489
+ elementSemanticText(element) === "" &&
490
+ normalize(element.parentElement?.parentElement?.textContent)
491
+ .length > 0,
492
+ ) ?? null
493
+ );
494
+ }
495
+ scope = scope.parentElement;
496
+ }
497
+ return null;
498
+ }
499
+
500
+ async function openExistingActionEditor(): Promise<void> {
501
+ const authLabels = ["Authentication", "身份验证", "认证"] as const;
502
+ if (clickable(authLabels)) return;
503
+ const edit = existingActionEditButton();
504
+ if (!edit) throw new Error("GPT_EDITOR_ACTION_EDIT_NOT_FOUND");
505
+ edit.click();
506
+ for (let attempt = 0; attempt < 120; attempt += 1) {
507
+ if (clickable(authLabels)) return;
508
+ try {
509
+ findControl(["OpenAPI schema", "Schema", "OpenAPI", "架构"]);
510
+ return;
511
+ } catch {}
512
+ await sleep(100);
513
+ }
514
+ throw new Error("GPT_EDITOR_ACTION_EDITOR_NOT_READY");
515
+ }
516
+
517
+ async function waitForAuthSettingsButton(): Promise<HTMLElement> {
518
+ const labels = ["Authentication", "身份验证", "认证"] as const;
519
+ for (let attempt = 0; attempt < 120; attempt += 1) {
520
+ for (const label of document.querySelectorAll<HTMLElement>("label")) {
521
+ if (!available(label) || !matchesExactSemantic(label, labels)) continue;
522
+ let container: HTMLElement | null = label.parentElement;
523
+ for (let depth = 0; depth < 4 && container; depth += 1) {
524
+ const buttons = [
525
+ ...container.querySelectorAll<HTMLElement>("button"),
526
+ ].filter(available);
527
+ const [button] = buttons;
528
+ if (buttons.length === 1 && button) return button;
529
+ container = container.parentElement;
530
+ }
531
+ }
532
+ await sleep(100);
533
+ }
534
+ throw new Error("GPT_EDITOR_AUTH_SETTINGS_BUTTON_NOT_FOUND");
535
+ }
536
+
537
+ async function returnFromActionEditor(): Promise<void> {
538
+ let back: HTMLButtonElement | undefined;
539
+ for (let attempt = 0; attempt < 80; attempt += 1) {
540
+ back = [...document.querySelectorAll<HTMLButtonElement>("button")].find(
541
+ (button) => {
542
+ if ((button.textContent ?? "").trim().length > 0) return false;
543
+ const context = normalize(
544
+ button.parentElement?.parentElement?.textContent ?? "",
545
+ );
546
+ return (
547
+ context.includes("add action") ||
548
+ context.includes("添加操作") ||
549
+ context.includes("edit action") ||
550
+ context.includes("编辑操作")
551
+ );
552
+ },
553
+ );
554
+ if (back) break;
555
+ await sleep(100);
556
+ }
557
+ if (!back) throw new Error("GPT_EDITOR_ACTION_BACK_NOT_FOUND");
558
+ back.click();
559
+ await waitForReadback("GPT_EDITOR_CONFIGURE_RETURN_TIMEOUT", () =>
560
+ [...document.querySelectorAll("label")].some((label) =>
561
+ matchesAny(label, ["Knowledge", "知识"]),
562
+ ),
563
+ );
564
+ }
565
+
566
+ async function configureBearerAuthDraft(credential: string) {
567
+ if (credential.length < 32) throw new Error("ROLE_CREDENTIAL_INVALID");
568
+ await openExistingActionEditor();
569
+ (await waitForAuthSettingsButton()).click();
570
+ let dialog: HTMLElement | null = null;
571
+ for (let attempt = 0; attempt < 600; attempt += 1) {
572
+ dialog =
573
+ [...document.querySelectorAll<HTMLElement>('[role="dialog"]')].find(
574
+ (candidate) => available(candidate),
575
+ ) ?? null;
576
+ if (dialog) break;
577
+ await sleep(100);
578
+ }
579
+ if (!dialog) throw new Error("GPT_EDITOR_AUTH_DIALOG_NOT_FOUND");
580
+ const authRadio = async (labels: readonly string[]) => {
581
+ for (let attempt = 0; attempt < 120; attempt += 1) {
582
+ for (const radio of dialog.querySelectorAll<HTMLElement>(
583
+ 'input[type="radio"], [role="radio"]',
584
+ )) {
585
+ let container: HTMLElement | null = radio.parentElement;
586
+ for (let depth = 0; depth < 3 && container; depth += 1) {
587
+ if (matchesBoundedSemantic(container, labels)) return radio;
588
+ container = container.parentElement;
589
+ }
590
+ }
591
+ await sleep(100);
592
+ }
593
+ throw new Error(
594
+ `GPT_EDITOR_AUTH_RADIO_NOT_FOUND:${labels[0] ?? "unknown"}`,
595
+ );
596
+ };
597
+ const apiKey = await authRadio(["API Key", "API 密钥"]);
598
+ if (!selected(apiKey)) apiKey.click();
599
+ const bearer = await authRadio(["Bearer"]);
600
+ if (!selected(bearer)) bearer.click();
601
+ let keyInput: HTMLInputElement | null = null;
602
+ for (let attempt = 0; attempt < 120; attempt += 1) {
603
+ const candidate = dialog.querySelector<HTMLInputElement>(
604
+ 'input[type="password"]',
605
+ );
606
+ if (candidate && available(candidate)) {
607
+ keyInput = candidate;
608
+ break;
609
+ }
610
+ await sleep(100);
611
+ }
612
+ if (!keyInput) throw new Error("GPT_EDITOR_AUTH_KEY_INPUT_NOT_FOUND");
613
+ setControlValue(keyInput, credential);
614
+ if (controlValue(keyInput) !== credential)
615
+ throw new Error("GPT_EDITOR_AUTH_KEY_READBACK_MISMATCH");
616
+ (await waitForAuthSemantic(dialog, ["Save", "保存"])).click();
617
+ await waitForReadback(
618
+ "GPT_EDITOR_AUTH_SAVE_TIMEOUT",
619
+ () => !available(dialog),
620
+ );
621
+ await returnFromActionEditor();
622
+ }
623
+
624
+ function savedConfirmationVisible(): boolean {
625
+ const labels = [
626
+ "Settings saved",
627
+ "设置已保存",
628
+ "GPT updated",
629
+ "GPT 已更新",
630
+ ] as const;
631
+ return [...document.querySelectorAll<HTMLElement>('[role="dialog"]')].some(
632
+ (dialog) =>
633
+ available(dialog) &&
634
+ labels.some((label) =>
635
+ normalize(dialog.textContent).includes(normalize(label)),
636
+ ),
637
+ );
638
+ }
639
+
640
+ async function finalizeBearerAuth(credential: string) {
641
+ await configureBearerAuthDraft(credential);
642
+ (await waitForAuthSemantic(document, ["Update", "更新"])).click();
643
+ await waitForReadback(
644
+ "GPT_EDITOR_AUTH_UPDATE_TIMEOUT",
645
+ savedConfirmationVisible,
646
+ );
647
+ const gptId = currentGptId();
648
+ if (!gptId) throw new Error("GPT_EDITOR_GPT_ID_MISSING");
649
+ return { status: "AUTH_UPDATED" as const, gptId };
650
+ }
651
+
652
+ async function selectPrivateVisibility(): Promise<void> {
653
+ let control: HTMLElement | null = null;
654
+ for (let attempt = 0; attempt < 80; attempt += 1) {
655
+ control = privateVisibilityControl();
656
+ if (control) break;
657
+ await sleep(100);
658
+ }
659
+ if (!control) throw new Error("GPT_EDITOR_PRIVATE_CONTROL_NOT_FOUND");
660
+ if (!selected(control)) control.click();
661
+ for (let attempt = 0; attempt < 40; attempt += 1) {
662
+ const current = privateVisibilityControl();
663
+ if (current && selected(current)) return;
664
+ await sleep(100);
665
+ }
666
+ throw new Error("GPT_EDITOR_PRIVATE_SELECTION_NOT_CONFIRMED");
667
+ }
668
+
669
+ async function waitForPrivateCreateAction(
670
+ initialCreateButton?: HTMLElement,
671
+ ): Promise<HTMLElement> {
672
+ const control = privateVisibilityControl();
673
+ const scopedRoot =
674
+ control?.closest<HTMLElement>(
675
+ '[role="dialog"], [role="menu"], [role="listbox"]',
676
+ ) ?? document;
677
+ const labelGroups = [
678
+ ["Save", "保存"],
679
+ ["Publish", "发布"],
680
+ ["Create", "创建"],
681
+ ] as const;
682
+ for (let attempt = 0; attempt < 40; attempt += 1) {
683
+ for (const labels of labelGroups)
684
+ for (const element of scopedRoot.querySelectorAll<HTMLElement>(
685
+ 'button, [role="button"]',
686
+ ))
687
+ if (
688
+ element !== initialCreateButton &&
689
+ available(element) &&
690
+ matchesBoundedSemantic(element, labels)
691
+ )
692
+ return element;
693
+ await sleep(100);
694
+ }
695
+ throw new Error("GPT_EDITOR_PRIVATE_CREATE_ACTION_NOT_FOUND");
696
+ }
697
+
698
+ function publishCreateButton(): HTMLElement | null {
699
+ return (
700
+ [...document.querySelectorAll<HTMLElement>('button, [role="button"]')].find(
701
+ (element) =>
702
+ available(element) &&
703
+ element.getAttribute("role") !== "radio" &&
704
+ element.closest('[role="radiogroup"]') === null &&
705
+ matchesExactSemantic(element, ["Create", "创建"]),
706
+ ) ?? null
707
+ );
708
+ }
709
+
710
+ function fieldReadbackMatches(
711
+ field: keyof typeof fieldLabels,
712
+ expected: string,
713
+ ): boolean {
714
+ try {
715
+ return controlValue(findControl(fieldLabels[field])) === expected;
716
+ } catch {
717
+ return false;
718
+ }
719
+ }
720
+
721
+ function starterReadbackMatches(values: readonly string[]): boolean {
722
+ const controls = conversationStarterControls(values.length);
723
+ if (controls.length < values.length) return false;
724
+ return values.every(
725
+ (value, index) => controlValue(controls[index] as HTMLElement) === value,
726
+ );
727
+ }
728
+
729
+ function modelReadbackMatches(value: string): boolean {
730
+ try {
731
+ const selector = findControl([
732
+ "Recommended model",
733
+ "推荐模型",
734
+ "推荐的模型",
735
+ value,
736
+ ]);
737
+ if (selector instanceof HTMLSelectElement) {
738
+ const option = selector.selectedOptions[0];
739
+ return Boolean(
740
+ option &&
741
+ (option.value === value ||
742
+ normalize(option.textContent).endsWith(`(${normalize(value)})`)),
743
+ );
744
+ }
745
+ return semanticValues(selector).some((candidate) =>
746
+ candidate.includes(normalize(value)),
747
+ );
748
+ } catch {
749
+ return false;
750
+ }
751
+ }
752
+
753
+ function capabilityReadbackMatches(
754
+ capability: CustomGptCapability,
755
+ expected: boolean,
756
+ ): boolean {
757
+ try {
758
+ const control = findControl(capabilityLabels[capability]);
759
+ if (control instanceof HTMLInputElement && control.type === "checkbox")
760
+ return control.checked === expected;
761
+ return (control.getAttribute("aria-checked") === "true") === expected;
762
+ } catch {
763
+ return false;
764
+ }
765
+ }
766
+
767
+ function knowledgeReadbackMatches(name: string): boolean {
768
+ const expected = normalize(name);
769
+ return [
770
+ ...document.querySelectorAll<HTMLElement>('[role="group"], button'),
771
+ ].some(
772
+ (element) =>
773
+ normalize(element.getAttribute("aria-label")) === expected ||
774
+ normalize(element.textContent) === expected,
775
+ );
776
+ }
777
+
778
+ function actionReadbackMatches(schema: string): boolean {
779
+ const match = /servers:\s*\n\s*-\s*url:\s*([^\s]+)/m.exec(schema);
780
+ if (!match?.[1]) return false;
781
+ try {
782
+ const host = new URL(match[1]).host;
783
+ return normalize(document.body.textContent).includes(normalize(host));
784
+ } catch {
785
+ return false;
786
+ }
787
+ }
788
+
789
+ async function verifyConfiguredMaterial(
790
+ material: CustomGptProvisioningRequest,
791
+ ): Promise<void> {
792
+ await waitForReadback("GPT_EDITOR_NAME_READBACK_MISMATCH", () =>
793
+ fieldReadbackMatches("displayName", material.displayName),
794
+ );
795
+ await waitForReadback("GPT_EDITOR_DESCRIPTION_READBACK_MISMATCH", () =>
796
+ fieldReadbackMatches("description", material.description),
797
+ );
798
+ await waitForReadback("GPT_EDITOR_INSTRUCTIONS_READBACK_MISMATCH", () =>
799
+ fieldReadbackMatches("instructions", material.instructions),
800
+ );
801
+ await waitForReadback("GPT_EDITOR_STARTERS_READBACK_MISMATCH", () =>
802
+ starterReadbackMatches(material.conversationStarters),
803
+ );
804
+ await waitForReadback("GPT_EDITOR_MODEL_READBACK_MISMATCH", () =>
805
+ modelReadbackMatches(material.recommendedModel),
806
+ );
807
+ for (const capability of [
808
+ "webSearch",
809
+ "imageGeneration",
810
+ "codeInterpreter",
811
+ ] as const)
812
+ await waitForReadback(
813
+ `GPT_EDITOR_CAPABILITY_READBACK_MISMATCH:${capability}`,
814
+ () =>
815
+ capabilityReadbackMatches(
816
+ capability,
817
+ material.capabilities[capability],
818
+ ),
819
+ );
820
+ await waitForReadback("GPT_EDITOR_ACTION_READBACK_MISMATCH", () =>
821
+ actionReadbackMatches(material.actionSchema),
822
+ );
823
+ for (const file of material.knowledgeFiles)
824
+ await waitForReadback(
825
+ `GPT_EDITOR_KNOWLEDGE_READBACK_MISMATCH:${file.name}`,
826
+ () => knowledgeReadbackMatches(file.name),
827
+ );
828
+ await waitForReadback(
829
+ "GPT_EDITOR_DRAFT_ID_NOT_READY",
830
+ () => currentGptId() !== null,
831
+ );
832
+ await waitForReadback(
833
+ "GPT_EDITOR_CREATE_NOT_READY",
834
+ () => publishCreateButton() !== null,
835
+ );
836
+ }
837
+
838
+ async function openPrivateCreateSurface(
839
+ initialCreateButton: HTMLElement,
840
+ ): Promise<void> {
841
+ initialCreateButton.click();
842
+ for (let attempt = 0; attempt < 200; attempt += 1) {
843
+ if (privateVisibilityControl()) return;
844
+ await sleep(100);
845
+ }
846
+ throw new Error("GPT_EDITOR_PRIVATE_CONTROL_NOT_FOUND");
847
+ }
848
+
849
+ async function waitForLiveCreatedResult() {
850
+ for (let attempt = 0; attempt < 320; attempt += 1) {
851
+ const gptId = currentGptId();
852
+ if (gptId && savedConfirmationVisible())
853
+ return { gptId, carrierUrl: `https://chatgpt.com/g/${gptId}` };
854
+ await sleep(125);
855
+ }
856
+ throw new Error("GPT_EDITOR_PRIVATE_CREATE_TIMEOUT");
857
+ }
858
+
859
+ async function finalizePrivateCreateSurface(initialCreateButton?: HTMLElement) {
860
+ await selectPrivateVisibility();
861
+ (await waitForPrivateCreateAction(initialCreateButton)).click();
862
+ return waitForLiveCreatedResult();
863
+ }
864
+
865
+ async function createPrivateGpt() {
866
+ const createButton = publishCreateButton();
867
+ if (!createButton) throw new Error("GPT_EDITOR_FORM_READY_STATE_LOST");
868
+ await openPrivateCreateSurface(createButton);
869
+ return finalizePrivateCreateSurface(createButton);
870
+ }
871
+
872
+ const domPort: CustomGptEditorPort = {
873
+ async setTextField(field, value) {
874
+ setControlValue(findControl(fieldLabels[field]), value);
875
+ },
876
+ async replaceConversationStarters(values) {
877
+ await replaceConversationStarters(values);
878
+ },
879
+ async selectRecommendedModel(value) {
880
+ const selector = findControl([
881
+ "Recommended model",
882
+ "推荐模型",
883
+ "推荐的模型",
884
+ value,
885
+ ]);
886
+ if (selector instanceof HTMLSelectElement) {
887
+ let option: HTMLOptionElement | undefined;
888
+ const normalizedValue = normalize(value);
889
+ for (let attempt = 0; attempt < 150; attempt += 1) {
890
+ option = [...selector.options].find((candidate) => {
891
+ const optionText = normalize(candidate.textContent);
892
+ return (
893
+ candidate.value === value ||
894
+ optionText === normalizedValue ||
895
+ optionText.endsWith(`(${normalizedValue})`)
896
+ );
897
+ });
898
+ if (option) break;
899
+ await sleep(100);
900
+ }
901
+ if (!option)
902
+ throw new Error(`GPT_EDITOR_MODEL_OPTION_NOT_FOUND:${value}`);
903
+ setControlValue(selector, option.value);
904
+ return;
905
+ }
906
+ selector.click();
907
+ (await waitForClickable([value])).click();
908
+ },
909
+ async setCapability(capability, enabled) {
910
+ const labels = capabilityLabels[capability];
911
+ let control: HTMLElement;
912
+ try {
913
+ control = findControl(labels);
914
+ } catch {
915
+ control = await waitForClickable(labels);
916
+ }
917
+ if (control instanceof HTMLInputElement && control.type === "checkbox") {
918
+ if (control.checked !== enabled) control.click();
919
+ if (control.checked !== enabled)
920
+ throw new Error(
921
+ `GPT_EDITOR_CAPABILITY_READBACK_MISMATCH:${capability}`,
922
+ );
923
+ return;
924
+ }
925
+ if ((control.getAttribute("aria-checked") === "true") !== enabled)
926
+ control.click();
927
+ if ((control.getAttribute("aria-checked") === "true") !== enabled)
928
+ throw new Error(`GPT_EDITOR_CAPABILITY_READBACK_MISMATCH:${capability}`);
929
+ },
930
+ async configureBearerAuth(value) {
931
+ await configureBearerAuthDraft(value);
932
+ },
933
+ async uploadKnowledge(files) {
934
+ await uploadKnowledge(files);
935
+ },
936
+ async verifyReady(material) {
937
+ await verifyConfiguredMaterial(material);
938
+ },
939
+ async installActionSchema(value) {
940
+ let schema: HTMLElement | null = null;
941
+ try {
942
+ schema = findControl(["OpenAPI schema", "Schema", "OpenAPI", "架构"]);
943
+ } catch {
944
+ const create = clickable([
945
+ "Create new action",
946
+ "New action",
947
+ "创建新操作",
948
+ ]);
949
+ if (!create) throw new Error("GPT_EDITOR_ACTION_CREATE_NOT_FOUND");
950
+ create.click();
951
+ for (let attempt = 0; attempt < 200; attempt += 1) {
952
+ schema = document.querySelector<HTMLElement>(
953
+ 'textarea[placeholder*="OpenAPI"]',
954
+ );
955
+ if (schema) break;
956
+ try {
957
+ schema = findControl([
958
+ "在此处输入你的 OpenAPI 架构",
959
+ "OpenAPI schema",
960
+ "Schema",
961
+ "OpenAPI",
962
+ "架构",
963
+ ]);
964
+ break;
965
+ } catch {
966
+ await sleep(100);
967
+ }
968
+ }
969
+ }
970
+ if (!schema) throw new Error("GPT_EDITOR_ACTION_SCHEMA_NOT_FOUND");
971
+ setControlValue(schema, value);
972
+ await waitForReadback(
973
+ "GPT_EDITOR_ACTION_SCHEMA_READBACK_MISMATCH",
974
+ () => controlValue(schema as HTMLElement) === value,
975
+ );
976
+ await returnFromActionEditor();
977
+ },
978
+ async createPrivate() {
979
+ return createPrivateGpt();
980
+ },
981
+ };
982
+
983
+ const editorDriver = createCustomGptEditorDriver(domPort);
984
+
985
+ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
986
+ if (message.type !== "PROFLOW_PROVISIONING_COMMAND") return;
987
+ if (!editorSurfaceReady()) {
988
+ sendResponse({ ok: false, error: "PROVISIONING_SURFACE_NOT_READY" });
989
+ return;
990
+ }
991
+ void (async () => {
992
+ try {
993
+ await ensureConfigureMode();
994
+ if (message.operation === "PROVISION_CUSTOM_GPT") {
995
+ const material = parseCustomGptProvisioningRequest(message.request);
996
+ const result = await editorDriver.provision(material);
997
+ sendResponse({
998
+ ok: true,
999
+ value: {
1000
+ ...result,
1001
+ provisioningInstanceId: provisioningSurface.instanceId,
1002
+ url: location.href,
1003
+ },
1004
+ });
1005
+ return;
1006
+ }
1007
+ if (message.operation !== "FINALIZE_CUSTOM_GPT_AUTH")
1008
+ throw new Error("PROVISIONING_OPERATION_UNSUPPORTED");
1009
+ const expectedGptId = carrierGptId(message.request.carrierUrl);
1010
+ let credential = message.request.credential;
1011
+ delete message.request.credential;
1012
+ if (typeof credential !== "string" || credential.length < 32)
1013
+ throw new Error("ROLE_CREDENTIAL_INVALID");
1014
+ try {
1015
+ const result = await finalizeBearerAuth(credential);
1016
+ if (result.gptId !== expectedGptId)
1017
+ throw new Error("GPT_EDITOR_AUTH_TARGET_MISMATCH");
1018
+ sendResponse({ ok: true, value: result });
1019
+ } finally {
1020
+ credential = "";
1021
+ }
1022
+ } catch (error) {
1023
+ sendResponse({
1024
+ ok: false,
1025
+ error:
1026
+ error instanceof Error
1027
+ ? error.message
1028
+ : "GPT_EDITOR_PROVISIONING_FAILED",
1029
+ });
1030
+ }
1031
+ })();
1032
+ return true;
1033
+ });