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 имеет доступ только к доменам из whitelist, которые должны пройти review;
  • какие домены можно трогать из виджета, вы явно объявляете через openai/widgetCSP в ответах MCP; иначе запросы просто не пройдут;
  • рекомендованный путь для всего серьёзного — вообще не дергать сеть из виджета, а ходить в backend через MCP-инструменты и callTool (об этом подробнее в Модуле 4).

Практически: думайте про виджет как про тонкий UI-слой. Он разговаривает с ChatGPT и вашим сервером через строго определённые каналы, а не как обычное SPA, свободно живущее в интернете.

Хранилища и ресурсы

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

Отсюда важный вывод: никаких тяжёлых вычислений и долгоживущих «кэшей» в виджете. Сложная логика — на стороне сервера, а не в React-компоненте.

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

Чтобы виджет вообще мог о чём-то узнать (результаты инструмента, режим отображения, локаль, состояние), ChatGPT при инициализации встраивает в окно iframe один глобальный объект — window.openai.

Это не ваш код и не npm-пакет, а host object, который предоставляет сама ИИ-платформа. Под капотом он завязан на события и сообщения между хостом и iframe, но вам про это почти не нужно думать. Важно помнить несколько моментов.

Кто и когда создаёт window.openai

window.openai появляется только:

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

Этот тип вы уже видели в модуле про HelloWorld App — именно его возвращает страница виджета вместо обычного text/html.

Если вы просто открываете страницу widget’а напрямую в браузере, то:

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

и это нормально. Поэтому в коде виджета всегда стоит проверять, что объект есть, если вы рассчитываете на «standalone»-режим для локальной разработки или сторибука.

Примитивный пример (не финальный, просто иллюстрация):

if (typeof window !== "undefined" && (window as any).openai) {
  console.log("We are inside ChatGPT sandbox!");
}

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

Под капотом ChatGPT обновляет window.openai по мере прихода новых данных (новый toolOutput, смена displayMode и т.д.), используя внутреннее событие openai:set_globals.

То есть «значения» в нём не статичны: ИИ-модель может вызвать MCP-инструмент, backend вернёт новые structuredContent, и window.openai.toolOutput поменяется прямо под вашим React-компонентом.

Отсюда две рекомендации:

  1. Не делать «слепые» snapshot’ы вида const toolOutput = window.openai.toolOutput один раз в начале и думать, что он вечен. Один и тот же виджет может быть переиспользован ChatGPT.
  2. Использовать слой хуков (чуть позже), который умеет подписываться на изменения.

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

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

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

window.openai = {
  // State & data
  toolInput,               // JSON: параметры которые ИИ передал в ваш MCP-tool
  toolOutput,              // JSON: параметры которые ваш MCP-tool вернул ИИ
  toolResponseMetadata,    // Ответ MCP-tool: часть _meta: {...}
  widgetState,             // Можно прчитать сохраненне состояние виджета
  setWidgetState,          // Можно соханить состояние вашего видежта сюда

  // Runtime APIs
  callTool,                // Можно вызвать MCP-tool
  sendFollowUpMessage,     // Скрытно написать сообщения для ИИ в чате: он начнет отвечать.
  requestDisplayMode,      // Переключить Виджет в другой mode: fullscreen, pip, inline
  requestModal,            // Превратить виджет в модальное окно.
  requestClose,            // Закрывает виджет. Закрывает модальное окно - превращается в виджет.
  requestCheckout,         // Открывает модальное окно для оплаты. Сервер должен реализовать ACP
  notifyIntrinsicHeight,   // Сообщение об изменени высоты виджета
  openExternal,            // Открыть ссылку в новом окне.

  // Context
  theme,                   // Теманя или светлая тема
  displayMode,             // Текущий режим отображения виджета, может отличаться от requestDisplayMode
  maxHeight,               // Максимально допустимая высота виджета
  safeArea,                // "Безопасная область отрисовки" - актуально для телефонов с "вырезами"
  view,
  userAgent,               // userAgent бразура
  locale                   // locale бразура
}

То же самое в табличке:

Категория Свойство / метод Для чего нужно
State & data
toolInput
Аргументы, с которыми был вызван инструмент. Read-only.
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 widget</h1>
      <p>Запущено внутри ChatGPT: {String(isChatGpt)}</p>
    </main>
  );
}

Откройте DevTools: либо прямо в окне ChatGPT (через встроенный viewer туннеля, если он это позволяет), либо в локальном браузере при прямом открытии страницы. В обоих вариантах сравните:

  • при запуске в обычном браузере 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/backend.

8. Типичные ошибки при работе в песочнице и с window.openai

Ошибка №1: считать, что виджет — это «обычный сайт в iframe».
Новички по привычке пытаются лезть в window.parent, менять стили ChatGPT или использовать localStorage как всегда. В песочнице это либо не работает, либо работает нестабильно: origin другой, storage изолирован, доступ к 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. В dev-окружении это оканчивается крашем «Cannot read properties of undefined». Используйте useIsChatGptApp, проверки на typeof window !== "undefined" и fallback-UI для случаев, когда виджета как такового нет.

Ошибка №6: игнорировать контекст среды (theme, displayMode, maxHeight, locale).
Можно, конечно, задать жёсткую высоту 2000px, тёмную тему всегда и верстать под десктоп — но это сделает опыт пользователя довольно странным. Платформа даёт вам сигналы о том, сколько места есть, какая тема и локаль — разумно ими пользоваться через useOpenAIGlobal, useDisplayMode, useMaxHeight и др., чтобы виджет выглядел «родным» в ChatGPT.

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

1
Задача
ChatGPT Apps, 3 уровень, 0 лекция
Недоступна
Реактивный OpenAI Globals Inspector (тема, локаль, userAgent)
Реактивный OpenAI Globals Inspector (тема, локаль, userAgent)
1
Задача
ChatGPT Apps, 3 уровень, 0 лекция
Недоступна
SafeExternalLink + демонстрация ограничений sandbox (без падений)
SafeExternalLink + демонстрация ограничений sandbox (без падений)
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ