Artifact 是什么
在 Hashee 里,“小程序”和”小游戏”都通过同一个机制实现:Artifact。
Agent 推一段结构化数据进对话,客户端用对应 renderer 把它渲染成可交互卡片,
用户操作通过 artifact_response 反向传给 Agent,Agent 用 artifact_update
推回新状态。
这一页讲 Artifact 的概念、与”web 应用”的对比、生命周期、以及为什么这套 设计能同时支持简单卡片(投票)和复杂应用(游戏面板)。
与 Web 应用的对比
| 维度 | Web 应用 | Hashee Artifact |
|---|---|---|
| 渲染层 | 浏览器 / WebView | 原生客户端 React Native + 内置 / 自定义 renderer |
| 通信层 | HTTP / WebSocket / RPC | E2EE 加密 + Hashee 协议 |
| 状态层 | 客户端 + 服务器 | Agent 持有真源 + 客户端只是”当前帧” |
| 部署 | 独立站点 / 路径 | 嵌入在聊天内,作为消息的一种 |
| 加密 | TLS(线传) | 端到端(payload 内容平台不可见) |
| 离线 | 缓存 / Service Worker | 自动(Hashee 客户端已缓存最近消息) |
| 推送 | Service Worker push | 内建(Agent 主动 send) |
| 生命周期 | 用户主动打开/关闭 | 跟随消息,TTL 默认 30 天 |
简言之:Artifact 把”应用”内嵌到聊天对话里,并把通信层换成端到端加密。 对开发者来说,最大好处是不用维护”另一个前端”——直接发数据,客户端帮你渲染。
subtype 全枚举
| subtype | 内置 renderer | 适合 |
|---|---|---|
info | InfoCard | 一次性提示 |
reply | ReplyCard | 结构化回复 |
app | AppRenderer (按 payload.ui 路由) | 完全自定义 UI(小程序 / 小游戏) |
form | FormCard | 表单输入 |
response | ResponseCard | 表单回响 |
tool_call | (内部) | Capability Manifest 工具调用 |
tool_response | (内部) | 工具结果回流 |
approval | ApprovalCard | 危险操作确认 |
progress | ProgressCard | 长任务进度 |
media | MediaCard | 多媒体聚合 |
详细参考 SDK / Artifact 入门。
本章节聚焦 subtype: "app"——它最灵活,是实现”小程序”和”小游戏”的关键。
“app” subtype 是怎么工作的
Agent 发送 ↓{ subtype: "app", payload: { ui: "vote_card", ← 客户端按这个字段选 React 组件 question: "...", options: [...] }} ↓客户端 ArtifactAppRenderer ↓按 ui 字段路由到对应组件: - "vote_card" → <VoteCard payload={payload} onAction={...} /> - "tic_tac_toe" → <TicTacToe ... /> - "2048" → <Game2048 ... /> - "<unknown>" → <UnknownCard payload={payload} />(fallback:JSON 显示) ↓组件 onAction 回调把用户操作打包成 artifact_response 发回 AgentV1 客户端内置 renderer 列表:
payload.ui | 渲染组件 | 用途 |
|---|---|---|
vote_card | VoteCard | 投票 / 单选 / 多选 |
progress_card | ProgressCard(也可走 subtype:"progress") | 进度条 |
tic_tac_toe | TicTacToe | 井字棋 |
2048 | Game2048 | 2048 |
guess_number | GuessNumber | 猜数字 |
text_form | TextForm(可走 subtype:"form") | 简易文本表单 |
| 其他 | UnknownCard | 显示 JSON + 一个 “Action” 按钮 |
V2 路线允许 Agent 上传自定义 renderer 包(沙盒 React Native 组件, 通过 capability_manifest 声明),但 V1 仅内置列表。
生命周期
Agent 调用 sendArtifact() ↓后端写 messages 表 (subtype="artifact_app", revision=0, expires_at=now+30d) ↓推送给会话所有成员的客户端 ↓客户端 renderer 渲染初始状态 ↓ ┌─ 用户操作 → artifact_response → Agent 处理 → updateArtifact (revision++) │ ↓ │ 客户端按 ref_artifact + revision 替换渲染 ↓ 循环直到: - 用户主动关闭(V1 没有"关闭"概念,artifact 跟消息走) - Agent 调 expireArtifact() 主动结束 - expires_at 到达 - 100 次 update 上限触发自动 expire为什么这套设计能同时支持简单和复杂
简单(投票卡):
- payload 是几十字节 JSON
- Agent 端无状态机(在内存的 Map / Set 就够)
- 一次创建 + 几次 update
复杂(2048 游戏):
- payload 是 16 个格子 + 分数 + 历史
- Agent 端有完整状态机(可放 Redis / Postgres)
- 每次 user action 触发 update
两者协议层面没有差别,只在 payload 大小与 Agent 端状态管理复杂度上不同。
为什么不直接给 Agent 一个 WebView
可以这么做(artifact 里放 url),但有几个问题:
| 问题 | WebView | Artifact |
|---|---|---|
| E2EE | 失效(WebView 内容可能上 CDN) | 维持(payload 端到端加密) |
| 性能 | WebView 启动 ~500ms | renderer ~10ms |
| 内存 | 每个 WebView ~50MB | 共享 React Native runtime |
| 一致性 | 不同平台 WebView 行为不同 | 统一 React Native 渲染 |
| 离线 | 需自己 cache | 自动 |
| 通信 | 需要桥(postMessage) | Agent 直接 send/update |
V1 不提供”WebView artifact”。如果你确实要嵌外部 web 应用,
建议用 subtype: "info" + 一个 actions: [{ url, label }] 跳到外部浏览器。
客户端 renderer 的安全沙盒
V1 内置 renderer 是 静态编译进客户端的 React Native 组件,没有 “agent 上传 renderer 代码”的能力(V2 路线)。所以:
- ✅ 你能自由设计 payload schema 来驱动现有 renderer
- ✅ 你能 fork 客户端添加自定义 renderer(自托管场景)
- ❌ 你不能让别人客户端”动态下载”你写的 renderer
V2 路线引入”renderer 包”(沙盒 + capability 声明 + 显式 install) 后将开放,但 V1 这是底线。
性能预算
| 操作 | 预期时长 |
|---|---|
| sendArtifact (Agent 端) | 5-10ms 加密管线 + 网络 |
| 客户端首次渲染 | 30-50ms(含解密 + React 挂载) |
| updateArtifact | 5-10ms 加密 + 网络 |
| 客户端 update 渲染 | 16ms(一帧) |
| artifact_response 往返 | 100-200ms(典型 RTT) |
游戏类应用建议每秒 update 不超过 5 次(200ms 一次),太频繁用户感知不到差别 但消耗带宽 / 后端配额。
限制
| 项 | 上限 |
|---|---|
| 单 artifact JSON | 64 KB |
| 同对话同时 alive artifact | 100 |
| 单 artifact update 次数 | 100 |
| TTL 默认 | 30 天 |
payload.ui 字符串长度 | 64 字符 |
下一步
- 第一个小程序 — 投票卡
- 状态化交互 — 2048 小游戏
- 设计准则
- Artifact 高级 (SDK) — artifact_update / revision / 冲突