JavaRush /Курси /ChatGPT Apps /Робота в пісочниці: обмеження, нюанси та window.openai

Робота в пісочниці: обмеження, нюанси та window.openai

ChatGPT Apps
Рівень 3 , Лекція 0
Відкрита

1. Що це за пісочниця і чому ваш віджет «у клітці»

Коли ChatGPT показує ваш віджет, він відображає його не як звичайний <iframe src="https://ваш-сайт">. Віджет запускається в керованій «пісочниці» — ізольованому iframe з окремим origin і жорсткими налаштуваннями безпеки.

Технічно це виглядає приблизно так:

flowchart TD
    User["Користувач у ChatGPT"]
    Chat["ChatGPT UI + модель"]
    Iframe["Ваш віджет
sandboxed iframe"] MCP["Ваш MCP / backend"] User --> Chat Chat -->|виклик tool| MCP MCP -->|structuredContent + _meta| Chat Chat -->|window.openai.*| Iframe Iframe -->|callTool / follow-up| Chat Chat --> MCP

Ваш код виконується лише всередині цього iframe, а доступ до решти світу відбувається через вузько контрольований API, який надає хост (ChatGPT). Віджет не повинен:

  • ламати сам ChatGPT (DOM, стилі, продуктивність);
  • порушувати приватність користувача;
  • безконтрольно звертатися до мережі.

Звідси випливають ключові обмеження пісочниці.

Ізоляція DOM і origin

Віджет працює на спеціальному домені пісочниці (наприклад, https://sandbox-apps.oaiusercontent.com) з атрибутом sandbox на iframe. Це означає, що:

  • ви не можете звертатися до window.parent або до document ChatGPT — отримаєте SecurityError;
  • міждоменні механізми на кшталт postMessage контролюються хостом;
  • будь-які спроби «підкрутити інтерфейс ChatGPT через CSS» приречені.

Мережа та обмеження CSP

Браузер і CSP‑політика від хоста обмежують доступ вашого віджета до мережі:

  • метод fetch має доступ лише до доменів зі списку дозволених, які мають пройти перевірку;
  • які домени можна використовувати у віджеті, ви явно оголошуєте через openai/widgetCSP у відповідях MCP; інакше запити просто не пройдуть;
  • рекомендований шлях для всього серйозного — узагалі не звертатися до мережі з віджета, а ходити на бекенд через MCP‑інструменти й callTool (про це докладніше в модулі 4).

На практиці сприймайте віджет як тонкий UI‑шар. Він взаємодіє з ChatGPT і вашим сервером через суворо визначені канали, а не як звичайне SPA, що вільно «живе» в інтернеті.

Сховища та ресурси

Локальні сховища (localStorage, sessionStorage) вам доступні, а от cookie — ні. Ураховуйте це під час розробки застосунку. Памʼять і CPU обмежені. Якщо ви вирішите всередині віджета перерахувати всі прості числа до мільярда, хост має повне право примусово зупинити роботу вашого iframe.

Звідси важливий висновок: жодних важких обчислень і довгоживучих «кешів» у віджеті. Складна логіка має бути на стороні сервера, а не в React‑компоненті.

2. window.openai: міст між віджетом і ChatGPT

Щоб віджет узагалі міг про щось дізнатися (результати інструмента, режим відображення, локаль, стан), ChatGPT під час ініціалізації вбудовує у вікно iframe один глобальний обʼєкт — window.openai.

Це не ваш код і не npm‑пакет, а обʼєкт хоста, який надає сама платформа ШІ. На рівні реалізації він привʼязаний до подій і повідомлень між хостом та iframe, але вам майже не потрібно про це думати. Важливо памʼятати кілька моментів.

Хто і коли створює window.openai

window.openai зʼявляється лише:

  • всередині того iframe, який ChatGPT створив для вашого віджета;
  • коли HTML‑шаблон надіслано з правильним mimeType (text/html+skybridge) і він пройшов усі перевірки.

Цей тип ви вже бачили в модулі про HelloWorld App — саме його повертає сторінка віджета замість звичайного text/html.

Якщо ви просто відкриваєте сторінку віджета безпосередньо в браузері, то:

console.log(window.openai); // undefined

і це нормально. Тому в коді віджета завжди варто перевіряти, чи існує цей обʼєкт, якщо ви розраховуєте на standalone‑режим для локальної розробки або Storybook.

Дуже простий приклад (не остаточний, лише для ілюстрації):

if (typeof window !== "undefined" && (window as any).openai) {
  console.log("Ми всередині пісочниці ChatGPT!");
}

Асинхронність ініціалізації

На рівні реалізації ChatGPT оновлює window.openai у міру надходження нових даних (новий toolOutput, зміна displayMode тощо) — через внутрішню подію openai:set_globals.

Тобто значення в ньому не статичні: модель ШІ може викликати MCP‑інструмент, бекенд поверне нові structuredContent, і window.openai.toolOutput зміниться прямо у вашому React‑компоненті.

Звідси дві рекомендації:

  1. Не робіть «сліпих» знімків на кшталт const toolOutput = window.openai.toolOutput один раз на початку й не розраховуйте, що значення залишиться незмінним. ChatGPT може повторно використати той самий віджет.
  2. Користуйтеся шаром хуків (трохи далі): він уміє підписуватися на зміни.

3. Анатомія window.openai: дані, API та контекст

Офіційна документація дає доволі компактну таблицю полів і методів window.openai. Спробуймо подати її в більш «людському» вигляді.

Основні поля та методи

window.openai = {
  // Стан & дані
  toolInput,               // JSON: параметри, які ШІ передав у ваш MCP-tool
  toolOutput,              // JSON: параметри, які ваш MCP-tool повернув ШІ
  toolResponseMetadata,    // Відповідь MCP-tool: частина _meta: {...}
  widgetState,             // Можна прочитати збережений стан віджета
  setWidgetState,          // Сюди можна зберегти стан вашого віджета

  // Runtime API
  callTool,                // Можна викликати MCP-tool
  sendFollowUpMessage,     // Непомітно надіслати повідомлення для ШІ в чаті: він почне відповідати.
  requestDisplayMode,      // Перемкнути віджет в інший mode: fullscreen, pip, inline
  requestModal,            // Перетворити віджет на модальне вікно.
  requestClose,            // Закриває віджет. Закриває модальне вікно — повертається до віджета.
  requestCheckout,         // Відкриває модальне вікно для оплати. Сервер має реалізувати ACP
  notifyIntrinsicHeight,   // Повідомлення про зміну висоти віджета
  openExternal,            // Відкрити посилання в новому вікні.

  // Контекст
  theme,                   // Темна або світла тема
  displayMode,             // Поточний режим відображення віджета, може відрізнятися від requestDisplayMode
  maxHeight,               // Максимально допустима висота віджета
  safeArea,                // «Безпечна зона відмалювання» — актуально для телефонів із «вирізами»
  view,
  userAgent,               // userAgent браузера
  locale                   // locale браузера
}

Те саме в таблиці:

Категорія Властивість / метод Для чого потрібно
State & data
toolInput
Аргументи, з якими було викликано інструмент. Лише для читання.
State & data
toolOutput
Ваш structuredContent із MCP‑відповіді. Те, що бачать віджет і модель.
State & data
toolResponseMetadata
_meta з відповіді. Видно лише віджету, модель цього не читає.
State & data
widgetState
Знімок UI‑стану, який ChatGPT зберігає між рендерами віджета.
State & data
setWidgetState(state)
Зберегти новий знімок widgetState синхронно.
Function
callTool(name, args)
Викликати MCP‑інструмент із віджета.
Function
sendFollowUpMessage({prompt})
Попросити ChatGPT надіслати повідомлення в чат від імені віджета. Він почне відповідати.
Function
requestDisplayMode(...)
Попросити в хоста inline / fullscreen / pip.
Function
requestModal({title})
Попросити відкрити модальне вікно.
Function
notifyIntrinsicHeight()
Повідомити, що висота контенту змінилася.
Function
requestCheckout(...)
Відкриває діалог оплати за протоколом ACP.
Function
openExternal({href})
Відкрити зовнішнє посилання в браузері користувача.
Context
theme, displayMode, maxHeight, safeArea, view, userAgent, locale
Сигнали оточення: тема, режим, доступна висота, локаль тощо.

Не обовʼязково запамʼятовувати все з цієї таблиці одразу — сприймайте її як «карту місцевості». Тепер розберімо все не як сухий довідник, а як послідовне пояснення.

toolInput і toolOutput: звідки беруться дані

Коли модель вирішує викликати ваш інструмент, вона формує JSON‑аргументи. Ці аргументи:

  • надходять на MCP‑сервер як input до обробника;
  • одночасно потрапляють у window.openai.toolInput у віджеті.

Після виконання інструмента сервер повертає:

  • structuredContent — структуровані дані для UI;
  • _meta — приватні дані лише для віджета;
  • content — текст для самої моделі, щоб вона могла «розповісти» користувачеві, що відбулося.

structuredContent стає window.openai.toolOutput, а _meta стає window.openai.toolResponseMetadata.

Міні‑приклад (чистий JS, без React):

const root = document.getElementById("root");

// Можна безпечно використовувати nullish-оператор
const gifts = window.openai.toolOutput?.gifts ?? [];

root.textContent = `Знайдено подарунків: ${gifts.length}`;

widgetState і setWidgetState: памʼять віджета

widgetState — це те, що платформа готова запамʼятати про ваш UI між рендерами й навіть між окремими кроками діалогу.

Приклади доречних речей для widgetState:

  • обраний подарунок;
  • поточне сортування (за ціною / за популярністю);
  • номер сторінки в списку.

Натомість не варто зберігати:

  • сирий відгук стороннього API;
  • зображення в base64;
  • секретні токени.

Важливо памʼятати дві речі:

  1. widgetState зберігається і передається моделі разом із контекстом, тому не зберігайте там нічого чутливого.
  2. Обсяг обмежений (приблизно 4 тисячі токенів), тож не робіть із нього міні‑базу даних.

Найпростіший приклад використання (безпосередньо, без хуків, на чистому JS):

const current = window.openai.widgetState ?? { selectedGiftId: null };

function selectGift(id) {
  window.openai.setWidgetState({ ...current, selectedGiftId: id });
}

У реальному коді ми будемо загортати це в React‑хуки.

Runtime API: callTool, sendFollowUpMessage та інші

Ці методи дають змогу віджету не лише «малюватися», а й взаємодіяти з діалогом і сервером.

Кілька типових сценаріїв:

  • callTool("search_gifts", { budget: 50 }) — користувач натиснув кнопку «Змінити бюджет», ви звернулися до сервера й оновили UI;
  • sendFollowUpMessage({ prompt: "Покажи ще дорожчі ідеї" }) — замість того щоб просити користувача вручну набирати текст, ви додаєте кнопку follow‑up, яка створює нове повідомлення в чаті;
  • requestDisplayMode({ mode: "fullscreen" }) — якщо inline‑режиму стало тісно, віджет може ввічливо попросити ChatGPT розгорнутися на весь екран;
  • openExternal({ href: "https://myshop.com/checkout?giftId=123" }) — перенаправити користувача на зовнішній сайт (checkout, профіль тощо) через перевірений канал.

Усе це проходить через ChatGPT, а не безпосередньо через інтернет.

Контекст середовища: тема, режим, висота, локаль

Поля на кшталт theme, displayMode, maxHeight, locale дають вам розуміння того, у якому оточенні працює віджет.

Наприклад:

const theme = window.openai.theme;          // "light" або "dark"
const mode = window.openai.displayMode;     // "inline" | "fullscreen" | "pip"
const maxH = window.openai.maxHeight;       // доступна висота
const locale = window.openai.locale;        // "en-US", "de-DE", ...

За допомогою цих сигналів ви можете:

  • підлаштовувати кольори та відступи під тему;
  • змінювати макет залежно від режиму (inline vs fullscreen);
  • локалізувати підписи в UI під мову користувача (про це ще буде цілий модуль).

Платформа дає вам сигнали про те, скільки місця є, яка тема та локаль. Розумно використовувати їх через useOpenAIGlobal, useDisplayMode, useMaxHeight та інші хуки, щоб віджет виглядав органічно в ChatGPT.

4. Хуки поверх window.openai: не звертаємося до глобального обʼєкта безпосередньо

Прямий доступ до window.openai зручний для прототипу, але швидко робить код заплутаним: потрібні підписки на події, перевірки на undefined, повторювані обгортки. Саме тому в Next.js‑шаблоні для Apps SDK є готовий набір React‑хуків: вони приховують деталі й роблять усе реактивним.

Типовий індекс хуків виглядає так:

// app/hooks/openai/index.ts
export { useCallTool } from "./use-call-tool";
export { useSendMessage } from "./use-send-message";
export { useOpenExternal } from "./use-open-external";
export { useRequestDisplayMode, useRequestModal, useRequestClose } from "./use-request-display-mode";
export { useRequestCheckout } from "./use-request-checkout";

// State hooks
export { useDisplayMode } from "./use-display-mode";
export { useWidgetProps } from "./use-widget-props";
export { useWidgetState } from "./use-widget-state";
export { useOpenAIGlobal } from "./use-openai-global";

export { useMaxHeight } from "./use-max-height";
export { useIsChatGptApp } from "./use-is-chatgpt-app";

Назви й точний шлях можуть трохи відрізнятися у вашому шаблоні, але ідея всюди одна: замість window.openai.* ви використовуєте хуки. Розберімо ключові.

useWidgetProps: вхід і вихід інструмента

useWidgetProps зазвичай повертає обʼєкт із даними, які потрібні віджету: toolInput, toolOutput, toolResponseMetadata і, іноді, додаткові прапорці на кшталт isLoading.

Приклад:

import { useWidgetProps } from "../hooks/openai";

type Gift = { id: string; title: string; price: number };

export function GiftList() {
  const { toolOutput } = useWidgetProps<{ gifts: Gift[] }>();
  const gifts = toolOutput?.gifts ?? [];

  if (!gifts.length) {
    return <div>Поки немає варіантів подарунків.</div>;
  }

  return (
    <ul>
      {gifts.map((g) => (
        <li key={g.id}>{g.title} — ${g.price}</li>
      ))}
    </ul>
  );
}

Жодного window.openai у коді компонента — і це добре.

useWidgetState: «реактивна обгортка» над widgetState

useWidgetState дозволяє працювати з widgetState як зі звичайним станом React: ви отримуєте [state, setState], а хук на рівні реалізації синхронізує його з window.openai.widgetState і setWidgetState.

Приклад:

import { useWidgetState } from "../hooks/openai";

type UiState = { selectedGiftId: string | null };

export function SelectedGiftIndicator() {
  const [uiState, setUiState] = useWidgetState<UiState>(() => ({
    selectedGiftId: null,
  }));

  if (!uiState?.selectedGiftId) {
    return <div>Подарунок ще не обрано.</div>;
  }

  return (
    <div>
      Ви обрали подарунок із id={uiState.selectedGiftId}
      <button onClick={() => setUiState({ selectedGiftId: null })}>
        Скинути
      </button>
    </div>
  );
}

Після клацання setUiState не лише оновить стан React, а й збереже новий стан на стороні ChatGPT.

useOpenAIGlobal: доступ до будь‑якого поля window.openai

Якщо потрібен доступ до одного глобального поля (наприклад, теми чи режиму), є універсальний хук useOpenAIGlobal(key). Він підписується на подію openai:set_globals і повертає завжди актуальне значення.

Приклад:

import { useOpenAIGlobal } from "../hooks/openai";

export function ThemeAwareBlock() {
  const theme = useOpenAIGlobal<"light" | "dark">("theme");

  const background = theme === "dark" ? "#222" : "#fff";
  const color = theme === "dark" ? "#fff" : "#000";

  return <div style={{ background, color }}>Я поважаю тему ChatGPT</div>;
}

useCallTool, useSendMessage, useOpenExternal та інші

  • useCallTool(name) — повертає функцію, яка викликає MCP‑інструмент із указаною назвою. Це обгортка над callTool.
  • useSendMessage() — обгортає sendFollowUpMessage, щоб віджет міг ініціювати повідомлення.
  • useOpenExternal() — зручний помічник над openExternal({ href }).
  • useRequestDisplayMode() і useRequestModal() — обгортки для запитів зміни режиму / відкриття модального вікна.

Базовий приклад міні‑віджета GiftGenius, який використовує майже все одразу:

import {
  useWidgetProps,
  useWidgetState,
  useCallTool,
  useSendMessage,
  useOpenExternal,
} from "../hooks/openai";

type Gift = { id: string; title: string; url: string; price: number };

export function GiftWidget() {
  const { toolOutput } = useWidgetProps<{ gifts: Gift[] }>();
  const gifts = toolOutput?.gifts ?? [];

  const [ui, setUi] = useWidgetState<{ selectedId: string | null }>(() => ({
    selectedId: null,
  }));

  const callSearch = useCallTool("search_gifts");
  const sendMessage = useSendMessage();
  const openExternal = useOpenExternal();

  if (!gifts.length) {
    return <div>Поки немає ідей. Спробуйте попросити GPT оновити результати.</div>;
  }

  return (
    <div>
      {gifts.map((g) => (
        <button
          key={g.id}
          style={{
            display: "block",
            fontWeight: ui?.selectedId === g.id ? "bold" : "normal",
          }}
          onClick={() => setUi({ selectedId: g.id })}
        >
          {g.title} — ${g.price}
        </button>
      ))}

      <div style={{ marginTop: 12 }}>
        <button
          onClick={() =>
            sendMessage({ prompt: "Покажи подарунки дорожчі за поточні." })
          }
        >
          Попросити ще ідеї
        </button>

        <button
          onClick={async () => {
            await callSearch({ budget: 200 });
          }}
        >
          Оновити з бюджетом $200
        </button>

        {ui?.selectedId && (
          <button
            onClick={() =>
              openExternal({
                href: `https://giftgenius.example.com/checkout?id=${ui.selectedId}`,
              })
            }
          >
            Перейти до покупки
          </button>
        )}
      </div>
    </div>
  );
}

Ця сторінка ще недопрацьована (у наступних модулях ми допрацюємо UX, обробку помилок тощо), але вже добре ілюструє підхід: жодних прямих звернень до window.openai, лише хуки.

5. Практика: вивчаємо пісочницю та window.openai

Щоб краще відчути, що таке «віджет не як звичайний сайт», корисно виконати кілька вправ.

Вправа: «Дослідіть середовище»

Візьміть ваш поточний app/page.tsx у віджеті й додайте туди під час першого рендера простий ефект:

import { useEffect } from "react";
import { useIsChatGptApp } from "../hooks/openai";

export default function Root() {
  const isChatGpt = useIsChatGptApp();

  useEffect(() => {
    if (typeof window !== "undefined") {
      console.log("window.origin =", window.origin);
      console.log("window.openai =", (window as any).openai);
    }
  }, []);

  return (
    <main>
      <h1>Віджет GiftGenius</h1>
      <p>Запущено всередині ChatGPT: {String(isChatGpt)}</p>
    </main>
  );
}

Відкрийте DevTools: або безпосередньо у вікні ChatGPT (через вбудований переглядач тунелю, якщо він це дозволяє), або в локальному браузері, відкривши сторінку напряму. В обох варіантах порівняйте:

  • під час запуску у звичайному браузері isChatGptApp буде false, а window.openai, найімовірніше, буде undefined;
  • під час запуску через ChatGPT ви побачите обʼєкт із полями toolInput, toolOutput, theme тощо.

Це дає добру інтуїцію: той самий React‑код поводиться по‑різному залежно від оточення, і саме для цього й потрібні хуки.

Вправа: «Виведіть усе, що дає платформа»

Додайте тимчасовий компонент для налагодження:

import { useWidgetProps, useOpenAIGlobal } from "../hooks/openai";

export function DebugPanel() {
  const { toolInput, toolOutput, toolResponseMetadata } = useWidgetProps();
  const theme = useOpenAIGlobal("theme");
  const displayMode = useOpenAIGlobal("displayMode");

  return (
    <pre style={{ fontSize: 10, maxHeight: 200, overflow: "auto" }}>
      {JSON.stringify(
        { toolInput, toolOutput, toolResponseMetadata, theme, displayMode },
        null,
        2
      )}
    </pre>
  );
}

І тимчасово додайте <DebugPanel /> під основним UI. Так ви наочно побачите:

  • які саме поля приходять із MCP у toolOutput;
  • що живе в _meta (наприклад, locale, userLocation та інше);
  • як змінюється displayMode, коли ви розгортаєте віджет.

Потім цей компонент можна прибрати або залишити ввімкненим через який‑небудь прапорець на кшталт DEBUG_WIDGET.

6. Взаємодія: ChatGPT ↔ віджет ↔ MCP/сервер

Щоб не сприймати віджет як «головного» учасника системи, корисно ще раз чітко зафіксувати ролі.

  1. Користувач пише повідомлення: «Підбери подарунок для дівчини, бюджет $50».
  2. Модель ChatGPT вирішує викликати ваш MCP‑інструмент search_gifts з аргументами { recipient: "girlfriend", budget: 50 }.
  3. MCP‑сервер виконує бізнес‑логіку й повертає:
    • content із коротким описом для моделі;
    • structuredContent з масивом подарунків;
    • _meta з технічними деталями (наприклад, source та валютою).
  4. ChatGPT:
    • показує користувачеві текстове повідомлення («Я знайшов кілька варіантів…»);
    • створює віджет‑iframe і передає туди structuredContent і _meta через window.openai.toolOutput і toolResponseMetadata.
  5. Ваш віджет:
    • рендерить UI за toolOutput;
    • під час взаємодій викликає callTool або надсилає follow‑up‑повідомлення;
  6. Модель далі вирішує, що робити з результатами цих дій.

Усе це підводить до важливої думки: віджет ніколи не є єдиним «господарем» процесу. Він — UI‑шар, який живе в екосистемі, що поєднує модель і MCP‑сервер. Складні речі (автентифікація, доступ до приватних даних, серйозна бізнес‑логіка) мають залишатися на стороні сервера. Віджет відповідає за зручний інтерфейс і коректне спілкування з користувачем.

7. Політики та правила гри в пісочниці

Уся ця конструкція з ізольованим iframe і window.openai існує не просто так, а через вимоги безпеки й приватності. Офіційні настанови OpenAI підкреслюють кілька принципів.

По‑перше, мінімізація даних. Ви не повинні через віджет намагатися зібрати з користувача якнайбільше PII (personally identifiable information) і передати її на свій бік. Усе, що справді необхідне, має бути чітко описане в інструментах, а модель і security‑шар уважно перевірятимуть такі виклики.

По‑друге, заборона на приховане стеження та fingerprinting. Не можна будувати систему спостереження за пристроєм користувача, збирати «відбитки» браузера чи шукати способи обійти обмеження. Параметри на кшталт userAgent, userLocation тощо — це підказки для UX, а не для автентифікації чи ідентифікації.

По‑третє, усе, що ви кладете в structuredContent, _meta, widgetState, у тій чи іншій формі або бачить користувач, або може побачити рецензент у Store. Тому:

  • жодні API‑ключі, токени, паролі та адмінські секрети туди класти не можна;
  • стан віджета потрібно проєктувати так, щоб користувач не дивувався, побачивши його в логах чи у відладці.

По‑четверте, мережеві виклики. Прямі запити з віджета до сторонніх API допустимі лише до суворо обмеженого списку доменів і в нечутливих сценаріях. Щойно йдеться про гроші, облікові записи, приватні дані — усе це має йти через MCP/бекенд.

8. Типові помилки під час роботи в пісочниці та з window.openai

Помилка №1: вважати, що віджет — це «звичайний сайт у iframe».
Часто з інерції хочеться звертатися до window.parent, змінювати стилі ChatGPT або використовувати localStorage як у звичайному застосунку. У пісочниці це або не працює, або працює нестабільно: origin відрізняється, сховище ізольоване, доступ до DOM блокується. Варто прийняти, що ви живете в керованому середовищі й спілкуєтеся з хостом лише через window.openai і хуки.

Помилка №2: звертатися до window.openai безпосередньо скрізь, де тільки можна.
Код на кшталт window.openai.toolOutput у десяти компонентах — шлях до застосунку, який складно налагоджувати. При цьому ви самі маєте стежити за подіями, асинхронністю й перевірками undefined. Значно надійніше відразу використовувати useWidgetProps, useWidgetState, useOpenAIGlobal та інші хуки, які вже обгортають openai:set_globals і синхронізують стан.

Помилка №3: зберігати в widgetState усе підряд (і особливо секрети).
Іноді хочеться «про всяк випадок» запхати туди величезний обʼєкт із результатами API або навіть токен доступу. У результаті зростає контекст, погіршується робота моделі, а ви порушуєте базові вимоги безпеки. widgetState має бути маленьким, містити лише UI‑сигнали й ніколи — конфіденційні дані.

Помилка №4: намагатися ходити в інтернет безпосередньо з віджета.
Виклики fetch("https://api.superbank.com/...") з пісочниці майже гарантовано натраплять на CORS. А навіть якщо ви налаштуєте все ідеально, це буде ненадійно й погано керовано. Усе, що повʼязане з реальними обліковими записами, грошима й персональними даними, потрібно реалізовувати як MCP‑інструменти й викликати їх через callTool або серверну частину.

Помилка №5: покладатися на стабільність window.openai поза ChatGPT.
Іноді розробники запускають віджет як окреме SPA і не додають перевірок на те, що window.openai може бути undefined. У середовищі розробки це закінчується падінням «Cannot read properties of undefined». Використовуйте useIsChatGptApp, перевірки на typeof window !== "undefined" і fallback‑UI для випадків, коли віджета як такого немає.

Помилка №6: ігнорувати контекст середовища (theme, displayMode, maxHeight, locale).
Можна, звісно, задати жорстку висоту 2000px, завжди вмикати темну тему й робити верстку лише під десктоп — але це зробить досвід користувача доволі дивним. Платформа дає вам сигнали про те, скільки місця є, яка тема та локаль — тож варто користуватися ними через useOpenAIGlobal, useDisplayMode, useMaxHeight та ін., щоб віджет виглядав органічно в ChatGPT.

Помилка №7: намагатися «обійти» політику через сторонні скрипти.
Іноді виникає спокуса підтягнути який‑небудь трекер, сторонній JS‑пакет або виконати код із чужого домену потайки. Пісочниця і CSP‑політики якраз зроблені, щоб такого не відбувалося: сторонні скрипти блокуються, а спроби обійти систему — прямий шлях до відхилення вашого App у Store.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ