Skip to content

存储

为什么存储很重要

Apps SDK会自动处理对话状态,但大多数真实应用还需要持久存储。潜在的用例包括:

  • 缓存获取的数据
  • 跟踪用户偏好
  • 持久化在component内创建的工件

自带后端

如果你有现有的API或需要多用户协作:

  • 通过OAuth验证用户身份
  • 使用后端API获取和修改数据
  • 返回结构化内容以供模型理解

关键考虑因素

在实施自定义存储时考虑:

  • 数据驻留和合规性 – 确保你的存储解决方案符合数据保护法规(GDPR、CCPA等)
  • 速率限制 – 实施速率限制以防止滥用并保护你的后端
  • 存储对象的版本控制 – 在更新数据架构时计划向后兼容性

示例: 集成后端API

typescript
server.registerTool(
  "fetch-user-prefs",
  {
    title: "获取用户偏好",
    securitySchemes: [{ type: "oauth2", scopes: ["prefs.read"] }],
    inputSchema: {},
  },
  async (args, context) => {
    const prefs = await fetch(`https://api.example.com/users/${context.subject}/prefs`, {
      headers: { Authorization: `Bearer ${context.accessToken}` },
    }).then((r) => r.json());

    return {
      content: [{ type: "text", text: "已加载用户偏好" }],
      structuredContent: { prefs },
    };
  }
);

示例: 保存用户数据

typescript
server.registerTool(
  "save-user-prefs",
  {
    title: "保存用户偏好",
    securitySchemes: [{ type: "oauth2", scopes: ["prefs.write"] }],
    inputSchema: { theme: z.string(), language: z.string() },
  },
  async ({ theme, language }, context) => {
    await fetch(`https://api.example.com/users/${context.subject}/prefs`, {
      method: "PUT",
      headers: {
        Authorization: `Bearer ${context.accessToken}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ theme, language }),
    });

    return {
      content: [{ type: "text", text: "已保存偏好" }],
      structuredContent: { theme, language },
    };
  }
);

持久化component状态

你的component可以将短暂的UI状态存储在 window.openai.widgetState 中,但持久的工件应存储在你的后端中。

使用 setWidgetState 存储短暂状态

tsx
const [selectedTab, setSelectedTab] = React.useState("overview");

React.useEffect(() => {
  window.openai.setWidgetState({ selectedTab });
}, [selectedTab]);

// 恢复状态
React.useEffect(() => {
  const savedState = window.openai.widgetState;
  if (savedState?.selectedTab) {
    setSelectedTab(savedState.selectedTab);
  }
}, []);

使用后端存储持久工件

对于用户希望跨会话保留的数据(例如草稿、设置、创建的内容),调用你的后端API:

tsx
async function saveDraft(content: string) {
  await window.openai.callTool("save-draft", { content });
}

async function loadDraft() {
  const result = await window.openai.callTool("load-draft", {});
  return result.structuredContent.draft;
}

优雅地处理合并冲突

如果用户在多个设备上打开相同的component,你的后端应检测冲突并提示用户解决它们:

typescript
server.registerTool(
  "save-draft",
  {
    title: "保存草稿",
    securitySchemes: [{ type: "oauth2", scopes: ["drafts.write"] }],
    inputSchema: { content: z.string(), lastModified: z.string() },
  },
  async ({ content, lastModified }, context) => {
    const existing = await getDraft(context.subject);

    if (existing && existing.lastModified > lastModified) {
      return {
        content: [{ type: "text", text: "检测到冲突。请刷新并重试。" }],
        structuredContent: { conflict: true, existing },
      };
    }

    await saveDraft(context.subject, content, new Date().toISOString());
    return {
      content: [{ type: "text", text: "已保存草稿" }],
      structuredContent: { saved: true },
    };
  }
);

操作提示

实施存储后,遵循这些最佳实践以确保可靠性和用户信任:

实施备份和监控

  • 定期备份用户数据
  • 监控存储操作是否有错误和延迟
  • 设置警报以便在失败率超过阈值时通知

设置明确的数据保留策略

  • 记录你存储数据的时间和原因
  • 实施自动清理不活跃数据
  • 为用户提供删除其数据的方法

在广泛发布之前彻底测试存储路径

  • 使用端到端测试验证读取和写入流程
  • 测试边缘情况,如并发写入、网络故障和大负载
  • 在进入生产之前使用小组用户进行测试

存储策略示例

策略1: 仅后端存储

所有持久数据都存储在你的后端中。Component通过工具调用读取和写入。

优点:

  • 完全控制数据
  • 易于备份和复制
  • 支持多用户协作

缺点:

  • 每次交互都需要网络往返
  • 需要后端基础设施

策略2: 混合存储

使用 widgetState 存储短暂的UI状态,使用后端存储持久工件。

优点:

  • 快速UI响应
  • 减少后端负载
  • 用户数据持久存在

缺点:

  • 需要管理两个存储层
  • Widget状态不跨会话持久存在

策略3: 仅客户端存储

仅使用 widgetState 的应用,不需要持久化。

优点:

  • 无后端基础设施
  • 无网络延迟

缺点:

  • 状态不跨会话持久存在
  • 无多用户协作
  • 数据丢失风险

数据库选择

根据你的用例选择数据库:

  • 关系型(PostgreSQL、MySQL) – 适合结构化数据和复杂查询
  • 文档型(MongoDB、DynamoDB) – 适合灵活的架构和快速迭代
  • 键值(Redis、Memcached) – 适合缓存和会话存储
  • 文件存储(S3、GCS) – 适合大型工件如图像和文档

许多应用组合使用多个存储层(例如,用于快速访问的Redis加上用于持久化的PostgreSQL)。

安全考虑

加密静态数据

敏感用户数据应在数据库中加密。使用AWS KMS或Google Cloud KMS等托管服务进行密钥管理。

加密传输中的数据

始终使用HTTPS进行API请求。验证你的后端拒绝HTTP连接。

实施访问控制

使用OAuth作用域和用户ID确保用户只能访问自己的数据。永远不要信任客户端提供的用户ID,始终从访问Token中提取它。

typescript
async function getDraft(userId: string, context: ToolContext) {
  if (context.subject !== userId) {
    throw new Error("未授权");
  }
  // 获取草稿...
}

审计日志

记录所有数据访问和修改以用于安全审计和调试:

typescript
async function saveDraft(userId: string, content: string, context: ToolContext) {
  await auditLog.write({
    action: "save_draft",
    userId,
    timestamp: new Date().toISOString(),
    ip: context.clientIp,
  });
  // 保存草稿...
}

性能优化

缓存频繁访问的数据

使用内存缓存(如Redis)来缓存频繁读取的数据:

typescript
async function getUserPrefs(userId: string) {
  const cached = await redis.get(`prefs:${userId}`);
  if (cached) return JSON.parse(cached);

  const prefs = await db.query("SELECT * FROM prefs WHERE user_id = $1", [userId]);
  await redis.setex(`prefs:${userId}`, 300, JSON.stringify(prefs)); // 缓存5分钟
  return prefs;
}

批量操作

对于多个读取或写入,使用批量操作来减少网络往返:

typescript
async function getMultipleDrafts(userIds: string[]) {
  return db.query("SELECT * FROM drafts WHERE user_id = ANY($1)", [userIds]);
}

使用连接池

数据库连接池可以减少连接开销并提高吞吐量:

typescript
import { Pool } from "pg";

const pool = new Pool({
  host: "localhost",
  database: "myapp",
  max: 20, // 最多20个连接
});

async function query(sql: string, params: any[]) {
  const client = await pool.connect();
  try {
    return await client.query(sql, params);
  } finally {
    client.release();
  }
}

后续步骤

有了存储策略,你可以安全地处理读取和写入场景,而不会损害用户信任。浏览示例页面以查看完整的存储实现,或查看部署指南以学习如何在生产中运行你的应用。