Appearance
存储
为什么存储很重要
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();
}
}后续步骤
有了存储策略,你可以安全地处理读取和写入场景,而不会损害用户信任。浏览示例页面以查看完整的存储实现,或查看部署指南以学习如何在生产中运行你的应用。
