主导航

状态管理

了解如何使用 Apps SDK 和 MCP 服务器在 ChatGPT 应用中管理业务数据、UI 状态和跨会话状态。

ChatGPT 应用中的状态管理

本指南介绍了在使用 Apps SDK 和 MCP 服务器构建应用时,如何为 ChatGPT 内呈现的自定义 UI 组件管理状态。您将了解如何确定各类状态所属的位置,以及如何跨渲染周期和对话持久化这些状态。

这些模式可确保您的 UI 与宿主无关,从而实现 MCP 应用“一次构建,多处运行”的方法。

概述

ChatGPT 应用中的状态分为三类

状态类型所属权生命周期示例
业务数据(权威数据)MCP 服务器或后端服务长效任务、工单、文档
UI 状态(临时)ChatGPT 内的组件实例仅限当前活跃组件选中行、展开面板、排序方式
跨会话状态(持久)您的后端或存储系统跨会话及跨对话已保存的筛选器、视图模式、工作区选择

将每种状态放在正确的位置,以保持 UI 的一致性,并确保对话与预期意图相匹配。


UI 组件如何在 ChatGPT 中存活

当您的应用返回一个自定义 UI 组件时,ChatGPT 会在绑定到特定对话消息的窗口组件 (widget) 内渲染该组件。只要该消息存在于线程中,该窗口组件就会一直保持。

关键行为

  • 窗口组件是消息作用域的: 每个返回窗口组件的响应都会创建一个带有自身 UI 状态的全新实例。
  • UI 状态随组件留存: 当您重新打开或刷新同一消息时,组件会恢复其已保存的状态(如选中行、展开面板等)。
  • 服务器数据是真理来源: 组件仅在工具调用完成后才会看到最新的业务数据,随后它会在此快照之上应用其本地 UI 状态。

心智模型

组件的 UI 层与数据层协同工作的方式如下

Server (MCP or backend)

├── Authoritative business data (source of truth)


ChatGPT Widget

├── Ephemeral UI state (visual behavior)

└── Rendered view = authoritative data + UI state

这种分离确保了 UI 交互的流畅性,同时保证了数据的准确性。


1. 业务状态(权威数据)

业务数据是真理来源。它应该存储在您的 MCP 服务器或后端,而不是组件内部。

当用户执行操作时

  1. UI 调用服务器工具。
  2. 服务器更新数据。
  3. 服务器返回新的权威快照。
  4. 组件使用该快照重新渲染。

这防止了 UI 与服务器之间的数据不同步。

示例:从 MCP 服务器(Node.js)返回权威状态

import { Server } from "@modelcontextprotocol/sdk/server";
import { jsonSchema } from "@modelcontextprotocol/sdk/schema";

const tasks = new Map(); // replace with your DB or external service
let nextId = 1;

const taskListOutputSchema = {
  type: "object",
  properties: {
    type: { type: "string", const: "taskList" },
    tasks: {
      type: "array",
      items: {
        type: "object",
        properties: {
          id: { type: "string" },
          title: { type: "string" },
          done: { type: "boolean" },
        },
        required: ["id", "title", "done"],
        additionalProperties: false,
      },
    },
  },
  required: ["type", "tasks"],
  additionalProperties: false,
};

const server = new Server({
  tools: {
    get_tasks: {
      description: "Return all tasks",
      inputSchema: jsonSchema.object({}),
      outputSchema: taskListOutputSchema,
      async run() {
        return {
          structuredContent: {
            type: "taskList",
            tasks: Array.from(tasks.values()),
          },
        };
      },
    },
    add_task: {
      description: "Add a new task",
      inputSchema: jsonSchema.object({ title: jsonSchema.string() }),
      outputSchema: taskListOutputSchema,
      async run({ title }) {
        const id = `task-${nextId++}`; // simple example id
        tasks.set(id, { id, title, done: false });

        // Always return updated authoritative state
        return this.tools.get_tasks.run({});
      },
    },
  },
});

server.start();

2. UI 状态(临时数据)

UI 状态描述的是数据的呈现方式,而非数据本身。

当新的服务器数据到达时,组件不会自动重新同步 UI 状态。相反,组件会保留其 UI 状态,并在权威数据刷新时将其重新应用。

使用 UI 框架的状态(React state、signals 等)将 UI 状态存储在组件实例内部。对于新应用:

  • 保持 UI 状态在 UI 内部局部化。
  • 当模型需要了解 UI 状态(选定的筛选器、暂存的编辑内容)时,请调用 ui/update-model-context

这能保持您的核心 UI 逻辑在兼容 MCP 应用的宿主环境间的可移植性。

ChatGPT 扩展(可选): 如果您希望 ChatGPT 为组件的生命周期持久化仅 UI 相关的状态,可以使用:

  • window.openai.widgetState – 读取当前的组件作用域状态快照。
  • window.openai.setWidgetState(newState) – 写入下一个快照。该调用是同步的,持久化会在后台进行。

由于宿主是异步持久化组件状态的,调用 window.openai.setWidgetState 时无需 await。请将其视为更新本地组件状态,并在每次有意义的 UI 状态更改后立即调用它。

示例(React 组件)

此示例演示了 ChatGPT 窗口组件状态的持久化(可选)。如果想在 React 中使用它,请将 window.openai.widgetStatewindow.openai.setWidgetState 封装在一个小型 Hook(例如 useWidgetState)中,并在您的项目中导入。

import { useWidgetState } from "./use-widget-state";

export function TaskList({ data }) {
  const [widgetState, setWidgetState] = useWidgetState(() => ({
    selectedId: null,
  }));

  const selectTask = (id) => {
    setWidgetState((prev) => ({ ...prev, selectedId: id }));
  };

  return (
    <ul>
      {data.tasks.map((task) => (
        <li
          key={task.id}
          style={{
            fontWeight: widgetState?.selectedId === task.id ? "bold" : "normal",
          }}
          onClick={() => selectTask(task.id)}
        >
          {task.title}
        </li>
      ))}
    </ul>
  );
}

示例(原生 JS 组件)

let tasks = [];
let widgetState = window.openai?.widgetState ?? { selectedId: null };

const updateFromToolResult = (toolResult) => {
  const nextTasks = toolResult?.structuredContent?.tasks;
  if (!nextTasks) return;
  tasks = nextTasks;
  renderTasks();
};

window.addEventListener(
  "message",
  (event) => {
    if (event.source !== window.parent) return;
    const message = event.data;
    if (!message || message.jsonrpc !== "2.0") return;
    if (message.method !== "ui/notifications/tool-result") return;
    updateFromToolResult(message.params);
  },
  { passive: true }
);

function selectTask(id) {
  widgetState = { ...widgetState, selectedId: id };
  window.openai?.setWidgetState?.(widgetState);
  renderTasks();
}

function renderTasks() {
  const list = document.querySelector("#task-list");
  list.innerHTML = tasks
    .map(
      (task) => `
        <li
          style="font-weight: ${widgetState.selectedId === task.id ? "bold" : "normal"}"
          onclick="selectTask('${task.id}')"
        >
          ${task.title}
        </li>
      `
    )
    .join("");
}

renderTasks();

组件状态中的图像 ID(模型可见图像,ChatGPT 扩展)

如果您的组件处理图像,请使用结构化的组件状态格式,并包含一个 imageIds 数组。宿主会将这些文件 ID 在后续对话中暴露给模型,以便模型对这些图像进行推理。

推荐的格式为:

  • modelContent:模型应看到的文本或 JSON。
  • privateContent:模型不应看到的仅 UI 相关的状态。
  • imageIds:由组件上传的文件 ID 列表,通过 window.openai.selectFiles()(当文件库可用时)选择,通过工具输入文件参数接收,或由工具文件引用返回。
type StructuredWidgetState = {
  modelContent: string | Record<string, unknown> | null;
  privateContent: Record<string, unknown> | null;
  imageIds: string[];
};

const [state, setState] = useWidgetState<StructuredWidgetState>(null);

setState({
  modelContent: "Check out the latest updated image",
  privateContent: {
    currentView: "image-viewer",
    filters: ["crop", "sharpen"],
  },
  imageIds: ["file_123", "file_456"],
});

只有通过 window.openai.uploadFile 上传、通过 window.openai.selectFiles()(当可用时)选择、通过文件参数接收或从工具结果文件引用中收到的文件 ID 才能包含在 imageIds 中。


3. 跨会话状态

需要在不同对话、设备或会话间持久化的偏好设置,应存储在您的后端。

Apps SDK 会自动处理对话状态,但大多数实际应用还需要持久化存储。您可能需要缓存获取的数据、跟踪用户偏好或持久化组件内创建的工件。选择添加存储层会增加功能,但也增加了复杂性。

自带后端

如果您已有 API 或需要多人协作,请与现有的存储层集成。在这种模型下:

  • 通过 OAuth 进行用户身份验证(请参阅身份验证),以便将 ChatGPT 身份映射到您的内部账户。
  • 使用您的后端 API 获取和修改数据。保持低延迟;用户期望组件在几百毫秒内渲染完成。
  • 返回结构化的内容,以便即便组件加载失败,模型也能理解数据。

当您自建存储时,请规划:

  • 数据驻留与合规性 – 在传输 PII 或受监管数据之前,确保已达成相关协议。
  • 速率限制 – 防止由于模型重试或多个活跃组件导致的 API 流量激增。
  • 版本控制 – 在存储对象中包含架构版本,以便您在不破坏现有对话的情况下进行迁移。

示例:组件调用工具

此示例假设您有一个 JSON-RPC 请求/响应助手(例如来自 快速入门),可以发送 tools/call 请求。

import { useState } from "react";

export function PreferencesForm({ userId, initialPreferences }) {
  const [formState, setFormState] = useState(initialPreferences);
  const [isSaving, setIsSaving] = useState(false);

  async function savePreferences(next) {
    setIsSaving(true);
    setFormState(next);

    // Use the MCP Apps bridge (`tools/call`) to invoke tools from the UI.
    // Ensure the tool is visible to the UI (app) in its descriptor (see
    // `_meta.ui.visibility`).
    const result = await rpcRequest("tools/call", {
      name: "set_preferences",
      arguments: { userId, preferences: next },
    });

    const updated = result?.structuredContent?.preferences ?? next;
    setFormState(updated);
    setIsSaving(false);
  }

  return (
    <form>
      {/* form fields bound to formState */}
      <button
        type="button"
        disabled={isSaving}
        onClick={() => savePreferences(formState)}
      >
        {isSaving ? "Saving…" : "Save preferences"}
      </button>
    </form>
  );
}

示例:服务器处理工具调用(Node.js)

import { Server } from "@modelcontextprotocol/sdk/server";
import { jsonSchema } from "@modelcontextprotocol/sdk/schema";
import { request } from "undici";

// Helpers that call your existing backend API
async function readPreferences(userId) {
  const response = await request(
    `https://api.example.com/users/${userId}/preferences`,
    {
      method: "GET",
      headers: { Authorization: `Bearer ${process.env.API_TOKEN}` },
    }
  );
  if (response.statusCode === 404) return {};
  if (response.statusCode >= 400) throw new Error("Failed to load preferences");
  return await response.body.json();
}

async function writePreferences(userId, preferences) {
  const response = await request(
    `https://api.example.com/users/${userId}/preferences`,
    {
      method: "PUT",
      headers: {
        Authorization: `Bearer ${process.env.API_TOKEN}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(preferences),
    }
  );
  if (response.statusCode >= 400) throw new Error("Failed to save preferences");
  return await response.body.json();
}

const preferencesOutputSchema = {
  type: "object",
  properties: {
    type: { type: "string", const: "preferences" },
    preferences: { type: "object" },
  },
  required: ["type", "preferences"],
  additionalProperties: false,
};

const server = new Server({
  tools: {
    get_preferences: {
      inputSchema: jsonSchema.object({ userId: jsonSchema.string() }),
      outputSchema: preferencesOutputSchema,
      async run({ userId }) {
        const preferences = await readPreferences(userId);
        return { structuredContent: { type: "preferences", preferences } };
      },
    },
    set_preferences: {
      inputSchema: jsonSchema.object({
        userId: jsonSchema.string(),
        preferences: jsonSchema.object({}),
      }),
      outputSchema: preferencesOutputSchema,
      async run({ userId, preferences }) {
        const updated = await writePreferences(userId, preferences);
        return {
          structuredContent: { type: "preferences", preferences: updated },
        };
      },
    },
  },
});

摘要

  • 在服务器上存储业务数据
  • 在组件内部存储 UI 状态(React state、signals 等)。当模型需要查看 UI 状态时使用 ui/update-model-context;仅当需要 ChatGPT 窗口组件状态持久化(可选)时使用 window.openai.widgetState / window.openai.setWidgetState
  • 跨会话状态存储在您管理的后端存储中。
  • 组件状态仅针对特定消息所属的组件实例持久化。
  • 避免使用 localStorage 存储核心状态。
© . This website operates independently and is not affiliated with or endorsed by OpenAI, Inc. All brand names, logos, and trademarks are the property of their respective owners.