PostHog Canvas 自由画布开发指南:从解析目标到受保护发布与构建的完整工作流
【免费下载链接】posthog:hedgehog: PostHog is the leading platform for building self-driving products. Our developer tools – AI observability, analytics, session replay, flags, experiments, error tracking, logs, and more – capture all the context agents need to diagnose problems, uncover opportunities, and ship fixes. Steer it all from Slack, web, desktop, or the MCP.项目地址: https://gitcode.com/GitHub_Trending/po/posthog
本篇指南基于 PostHog 仓库中 Canvas 产品的building-canvases技能文档,系统讲解如何为 PostHog 创建或编辑freeform自由画布——一种运行在沙箱 iframe 中的独立浏览器应用(数据看板、文档、表单、小工具、图形实验)。读完你可以掌握:如何解析或创建目标画布、如何在 React + Quill 与纯 HTML 两种实现路线间选型、图片资源的标准处理流程、"读 → 改 → 校验 → 发布 → 等构建"的迭代闭环,以及ph.state、ph.actions、ph.connectors、ph.agent.request四个运行时桥接 API 的声明规则;并进一步结合后端模型与校验源码,理解平台契约(固定依赖、能力声明、CSP 沙箱)的落地机制。
画布是什么:源码存在 PostHog 里的浏览器应用
Canvas(画布)是一个客户端浏览器应用,运行在 PostHog 内部的沙箱 iframe中。与常规前端工程最本质的区别是:它的源码存放在 PostHog 里,而不是代码仓库中。你通过canvas-*系列工具读写它,通过工具发布才算保存——永远不要把画布写到本地文件。
画布工作可以从任何普通任务发起,不要求存在专门的画布模式或预先创建的画布。只要用户要求"一个应该住在 PostHog 里的看板、文档、表单、可视化或小应用",就应当按画布请求处理。
PostHog 中画布分为三种类型(对应后端模型Canvas.kind字段的三个取值,见 models.py):
| 类型 | 含义 | 由哪个技能负责 |
|---|---|---|
freeform | 独立应用,源项目编译为单个制品 | building-canvases(本文主体) |
grid | 组件网格,包括用户的首页画布 | composing-grid-canvases |
component | 可复用组件(widget),被 grid 画布放置 | composing-grid-canvases |
building-canvases技能只拥有freeform画布。当目标是 grid 或首页画布、某个 placement 布局、或可复用组件时,应改加载 composing-grid-canvases——它拥有"商店搜索 → 配置 → fork → 构建"的阶梯与布局补丁循环。但注意:编写组件的源码仍然使用本文所述的实现类伴随技能。从源码结构看,这一划分与后端模型注释完全一致:grid的"源码"是布局文档,发布时校验并版本化布局而不排队构建;component与freeform共用同一套源码/构建管线,额外携带配置 schema 与网格尺寸契约。
第一步:解析目标画布
在动笔写任何代码之前,先确定"写给哪个画布"。规则按优先级排列:
- 任务指定了 canvas id(画布发起的任务都会带上):那就是目标,不要再创建新的。
- 否则目标频道是任务创建时所在的频道——任务上下文(
channel_context块或生成指令)中会写明频道名。用canvas-list工具(以channel参数限定范围)列出该频道的画布。如果其中有一个明显是所指对象(同一看板的早期迭代、同一工具的前身),就在其上继续构建而不是创建一个近似的重复品,并在回复中说明结果落在哪里。 - 只有当现有画布都不匹配时,才用
canvas-create在同一频道创建新画布,名字取请求中简短的描述性标题——绝不能用 "Untitled canvas"。 - 永远不要自行浏览频道来挑选目标:
channel-list只用于把用户点名的频道解析为 id。它的列表会把个人频道#me排在最前,而#me永远不应作为默认值——放在那里的画布对其他人不可见。如果任务既没有点名画布也没有点名频道,直接询问用户用哪个频道,而不是猜测。
canvas-create支持kind参数:freeform(独立应用,默认)、component(可复用组件,其已发布项目必须声明component放置契约)、grid(组件组合,通过canvas-layout-patch编辑)。工具定义见 mcp/tools.yaml。
选择实现路线:加载伴随技能
building-canvases拥有"画布选择 + 创作生命周期",而实现契约放在四个伴随技能中。在写源码之前,先加载所有适用的伴随技能:
building-react-quill-canvases——用于看板、数据板、表单、工具、应用式状态,或任何希望看起来"原生"于 PostHog 的场景。它拥有允许导入清单、Quill 组件组合、主题化、图表、加载/错误状态与日期选择器。building-html-canvases——用于文档、文章、聚焦实验、生成式图形、<canvas>或 WebGL,即应用组件不带来任何有用结构的场合。它拥有语义化标记、直接浏览器 API、动画清理与非 Quill 主题化。querying-canvas-data——只要画布要读 PostHog 数据、埋点或导航就必须加载。它拥有phSDK、"优先用已保存 insight"的数据层级、结果形状、变量、日期范围、按查询渐进加载与声明数据能力。涉及数据时,与任一实现技能一起加载。validating-and-publishing-canvases——每个画布都要加载。它拥有项目形状、能力声明、校验诊断、受保护发布、草稿、构建与冲突恢复。
实现方式可以混搭:React 可以拥有应用外壳,而浏览器图形代码拥有画布元素;或一个基本静态的页面挂载一个交互式"岛屿"。这是一个判断决策而不是持久化模式——只有当选择改变了一个你无法推断的用户可见需求时,才向用户提问。
从源码结构看,两种路线共享同一入口约束:building-html-canvases 明确要求所有画布都保留src/canvas.tsx作为被挂载的 React 入口组件(默认导出、无 props),React 层保持薄壳,体验写在其中的 HTML/CSS/浏览器 API 里——纯 HTML 路线的文档就是"效果上是语义化 HTML 的 JSX"。
图片资源:走公共媒体库,不要 base64
画布中的图片使用公共媒体库 URL。标准流程:
- 先调用
posthog:media-images-list(带purpose="canvas"),已有合适的图片就直接复用。 - 要添加本地图片时,调用
posthog:media-image-upload-start,传文件名字段和purpose="canvas"。 - 在 shell 中把文件以 multipart 表单数据 POST 到返回的
upload_url。包含所有返回的form_fields条目,并把文件部分放在最后。 - 调用
posthog:media-image-upload-complete,传回返回的 id,用其永久url作为图片src。 - 把该 URL 的精确 origin 加入
project.capabilities.network.origins。画布校验会检查这个声明,已发布制品会在其 Content Security Policy 中使用它。
约束与安全边界:
- 画布媒体 URL 是公开的、不需要认证。绝不要上传秘密、凭证、客户数据或敏感截图。
- 图片必须小于 4 MB,且可解码为 PNG、JPEG、GIF、WebP、AVIF 或 BMP。
- 绝不把图片字节 base64 编码进工具调用。
从后端实现看,第 5 步之所以强制,是因为声明的 origin 会被直接拼进入口制品的 CSP:contract.py 中artifact_csp()把通过canonical_network_origin()校验的 origin 注入connect-src、style-src、img-src、font-src、media-src、frame-src指令。而canonical_network_origin()本身是一道安全闸:只接受精确 HTTPS origin(无路径、无凭证、无查询/片段、无通配符),并且主机必须是公共的——回环地址、私有 IP、单标签域名(如intranet)、以及.local/.localhost/.internal/.home.arpa后缀全部被拒绝。因此像https://localhost:8010这样的本地开发主机会以invalid_network_origin诊断失败校验。
常见请求模式:路由示例而非固定模板
这些模式用于把请求路由到实现方案,不是必须套用的模板:
- 产品看板、Web 分析板、指标浏览器:React + Quill + 数据查询(
querying-canvas-data)。 - 清单、表单、轻量工作流:React + Quill;如需 PostHog 读取、埋点或导航,再加数据查询。对于清单或 runbook,从
building-react-quill-canvases的实例 references/checklist-example.md 起步——通过每步一个ph.state键实现团队共享进度。不要暗示现有 API 并不提供的持久化。 - 文档或叙述式报告:HTML 提供基本静态的阅读体验;需要 PostHog 实时数据、过滤器或应用式交互时改用 React + Quill + 数据查询。
- 生成式图形或动画:HTML + 浏览器图形 API;只有当 React 能实质简化应用状态或外壳时才加入 React。
如果任务携带诸如dashboard或web-analytics之类的遗留请求模式,应用上面匹配的形态。模式只是提示;用户的实际请求始终是权威。
迭代循环:读 → 改 → 校验 → 发布 → 等构建
这是整个技能的操作核心。完整循环如下:
1. 读取当前源码与版本指针
用canvas-source-retrieve读取当前源码与版本指针,记住current_version_id——你的发布必须用它做守卫。返回的项目形状为:schemaVersion(1)、files(路径 → 内容)、entryHtml("index.html")、dependencies(平台固定精确版本)、canvasSdkVersion、capabilities。从未发布过的画布current_version_id为null,首次发布时原样传null。
2. 按选定的实现技能编辑项目文件
对画布展示的任何 PostHog 数据,遵循querying-canvas-data技能:数据只走ph桥(已保存 insight 通过phSDK 加载——绝不自行fetch或自带 PostHog 客户端),且让每个数字可验证:insight 支撑的指标通过ph.openExternal链接其已保存 insight,ad-hoc 查询在卡片旁展示实际执行的精确查询。同时在project.capabilities中声明每一个ph调用:
- insight 短 id 进
capabilities.posthog.insights; - 埋点事件名进
capabilities.posthog.captureEvents; - ad-hoc 查询置
inlineQueries: true; ph.agent.request置agentRequests: true。
宿主在运行时强制这些声明,校验拒绝未声明的调用。校验逻辑在 source.py 中可以逐条对照:_validate_capabilities()用正则扫描源码中的ph.loadInsight/ph.query/ph.capture/ph.state/ph.actions.invoke/ph.agent.request/ph.connectors.call调用点,与声明清单比对后产出capability_missing_insight、capability_missing_inline_queries、capability_missing_capture_event、capability_missing_state、capability_missing_action、capability_missing_agent_requests、capability_missing_connector等 error 级诊断。
3. 校验直到干净
canvas-validate-create无副作用,可以随需随调;修复所有 error 级诊断后才能发布。诊断条目带severity、稳定的code、message,以及文件级问题的path与line。常见 error:import_not_allowed(裸导入被限制在源项目返回的依赖集内)、forbidden_dynamic_import/forbidden_require/forbidden_inline_script、invalid_path、各类capability_missing_*、dependency_not_admitted/dependency_version_mismatch、platform_token_redeclared(声明了与 Quill 平台 token 同名的 CSS 变量——平台样式表把--background、--border、--muted、--primary等设置在每个元素上,:root或html.dark里同名声明永远到不了任何元素,导致文字颜色失效;自己的变量必须加前缀),以及路径/尺寸越限。warning 级(如network_fetch/network_xhr)不阻断,但意味着代码在直接伸手网络,应声明精确 origin 或改用ph桥。
4. 受保护地发布(publish 即保存,默认且立即生效)
发布是保存变更的默认方式,首次版本与后续编辑一视同仁,且立即生效:
- 首版(
current_version_id为 null):用canvas-publish-create发布完整项目,传expected_current_version_id: null。 - 已上线(
current_version_id已设置):用canvas-edit-create按文件发布变更(每个 operation 设置某文件的完整内容,content: null表示删除),或用canvas-publish-create发布完整项目——都把当前current_version_id作为expected_current_version_id传入。canvas-edit-create的守卫是强制的,因为 diff 的语义依赖其基线。 - 草稿
canvas-draft-create仅在用户要求草稿、预览或上线前评审时暂存。草稿是一个真实的、可构建的版本但永远不会成为 head:在线画布继续渲染当前版本,直到有人 promote 它。草稿响应会返回capability_widening——草稿相对 live 版本新增声明的 insights、埋点事件、内联查询与网络 origin,应在 promote 前向用户明示(这是变更将新授予的访问面)。这个"能力扩张"信号由后端 capabilities.py 的capability_widening()计算,按"after 相对 before 新增了哪些声明"结构化输出。
发布约定:一次请求变更只发布一次;用户在之后要求再改时,重新读取源码(head 可能已移动)再发布——不要把无关变更打进同一版本,也不要在每次微编辑后发布半成品。429 表示团队构建容量暂时耗尽,等待约 30 秒后重试同一发布,此时尚未保存任何东西。发布响应返回新的current_version_id。
版本语义:每次发布向 append-only 的版本序列追加一个完整源版本并移动 head 指针(CanvasSourceVersion行只增不改,内容存对象存储,行上记录 SHA-256source_hash、能力清单快照与任务归属,见 models.py)。用户可以在应用里回退到旧版本(回退会重新发布并重新构建)。守卫的意义正在于此:基于你实际读到的版本来发布,才能避免用户的回退、其他 agent 的发布和你的编辑互相静默抹除。
409 version_conflict 的恢复流程:409 意味着画布已越过你的基线(并发发布或回退),响应里包含 livecurrent_version_id。绝不无守卫重试硬推:重新canvas-source-retrieve读取源码 → 在新鲜源码上重新应用你的编辑(新 head 可能含他人变更,必须保留)→ 用新的current_version_id重新发布。
5. 等待构建完成
草稿和发布一样都会排队一个服务端构建。轮询canvas-builds-retrieve(每几秒一次,最长约 2 分钟),直到你的构建进入终态:
queued/building——进行中,稍后再轮询;ready——画布的published_build_id前移到该构建(除非更新的发布已抢先取代它),画布工作完成;failed——读取构建的错误诊断,修复项目,再次保存。不要带着失败的构建结束任务。
从后端模型看为什么"失败的构建不会顶掉旧版":CanvasBuild注释明确写了——失败构建只记录诊断,永远不取代画布最后已知良好的制品;live 指针(Canvas.published_build)只在构建完成且其源版本仍是当前 head时才前移。这意味着如果你在这里收工,用户拿到的是陈旧画布加一次静默失败。
另有一条经验规则:运行时错误报告(渲染中画布抛错时上报到创作任务)会注明其来源构建 id。来自旧构建 id的报告是历史,不是你当前代码的证据——特别地,"某个已文档化的phAPI 未定义"(如ph.state)意味着该制品由旧宿主运行时打包,重新发布让当前构建替换它即可;永远不要通过删掉该 API 或其能力声明来"修复"。
运行时桥接:状态、动作、连接器与 agent 请求
自由画布通过宿主注入的ph桥与 PostHog 交互。以下四组 API 各有声明要求,未声明者校验失败、宿主运行时拒绝。
ph.state —— 持久键值记忆
- API:
ph.state.get(key, { scope })、ph.state.set(key, value, { scope })(值为 null 删除键)、ph.state.list({ scope })。 - 作用域:
"user"(默认)对每位查看者私有;"shared"每画布一个值、团队可见。使用的 scope 必须声明在capabilities.posthog.state。 - 约束:值是 JSON,序列化上限64 KB、每 scope 最多256 个键。大数据应存进 PostHog(insight、warehouse)再引用;state 里绝不放秘密或查看者 PII。
这两个数字不是文档随口说的:它们就是平台契约 manifest.json 中limits.maxStateValueBytes: 65536与maxStateKeysPerScope: 256的落地,后端CanvasState模型在写入时执行边界约束,使每次访问都是点查、表增长以画布数为上限。
ph.actions.invoke(verb, payload) —— 以查看者身份写 PostHog
- 每个 verb 必须声明在
capabilities.posthog.actions;未声明或未注册的 verb 校验失败,宿主运行时也拒绝。 - 只把动作接到显式用户手势(查看者点击的按钮),绝不接到 load 或 render。
- 动作注册表是唯一事实来源:用
canvases-actions-retrieve工具列出它,接线前遵循每个 verb 的usage(payload/结果形状、行为、它配什么样的确认文案)。
ph.connectors.call(provider, tool, args) —— 读取实时第三方数据
- 用查看者本人的连接(GitHub,或任意 MCP 商店服务器)在查看时读取数据。绝不要自己调用 GitHub、Calendly 或别的再把结果粘进源码:那样的快照在发布时就已陈旧,且会把作者的数据展示给每个查看者。
- 每个 provider 与 tool 声明在
capabilities.connectors;用canvas-connectors-retrieve工具发现它们。 - 校验侧(
_validate_connector_declarations/_validate_connector_calls):provider 必须是原生 id(如github)或mcp:<server host>形式;未知 provider、未注册的原生工具、私有 MCP 主机都会失败校验;每个声明的工具在目录中必须is_read_only: true。带 connectors 的画布不能声明 shared state(对应诊断connector_results_in_shared_state)。
ph.agent.request(prompt) —— 向创作 agent 请求变更
- 声明
capabilities.posthog.agentRequests: true。 - 只能从直接点击或表单提交调用——宿主会展示精确 prompt 并要求查看者接受后才消耗算力;渲染、挂载或轮询期间发起的调用会被拒绝。
- agent 以新版本发布该变更;非创建者的请求会被归档到创作任务线程而不是直接启动运行。
源项目形状与平台契约
源码项目本身受一组平台契约约束,其单一事实来源是 canvas_builder/manifest.json——它同时被 Node 构建器、Python 校验器(source.py)与制品源的 CSP 加载;桌面端应用还会在契约测试中用它断言自己的副本,防止固定依赖或限制漂移。
项目结构约定(来自building-canvases的 "Source-project shape" 节):
- 保留
index.html作为源工具返回的入口 shell(entryHtml必须是"index.html",校验代码invalid_entry/missing_entry强制这一点)。 src/canvas.tsx是约定的 React 入口组件,但它可以从项目内导入额外的相对 TypeScript、TSX、JavaScript、JSON、SVG、CSS 与已接纳的资产文件。- 自包含的模块 worker 可用
./worker.ts?worker导入;worker 不得再导入其他本地模块。 - 图片走上述媒体库流程;其他二进制资产放入项目的
assets映射,以 base64 内容 + 已接纳的 content type 表示。WOFF/WOFF2、WebAssembly 与通用 octet-stream 资产受支持。 - 平台依赖映射必须原样保留,不得添加 npm 包:相对导入是项目文件,裸导入被限制在平台固定集合内。
允许导入的裸模块(allowedImportSpecifiers):@posthog/canvas-sdk、react、react-dom、react-dom/client、@posthog/quill、recharts、lucide-react、dayjs、d3、three、framer-motion、zod、@tanstack/react-table、@tanstack/react-virtual、react-hook-form、lodash-es、react-markdown、papaparse。固定版本包括 react 19.0.0、@posthog/quill 0.3.0-beta.18、recharts 2.15.0 等;依赖缺失报dependency_not_admitted,版本漂移报dependency_version_mismatch。
源码体量限制(limits,校验器逐条强制执行):
| 限制 | 值 | 对应诊断 |
|---|---|---|
maxSourceFiles | 64 个文件 | too_many_files |
maxSourceFileBytes | 512 KB / 文件 | file_too_large |
maxSourceTotalBytes | 2 MB(files + assets 及规范化 JSON 双重计量) | file_too_large/project_too_large |
maxStateValueBytes | 64 KB / state 值 | 写入时边界 |
maxStateKeysPerScope | 256 键 / scope | 写入时边界 |
运行时安全规则(校验器中的禁止模式,全部 error 级):动态import()报forbidden_dynamic_import;require()报forbidden_require;importScripts()报forbidden_import_scripts;内联<script>报forbidden_inline_script。路径必须相对、非空、正斜杠,段字符集限字母数字. _ @ -,禁止.、..段(invalid_path)。远端脚本与动态导入被彻底封死——沙箱 CSP 本身就包含script-src 'self'、default-src 'none'、connect-src 'none'(声明 origin 后才有条件放开)。
收尾:链接与交付约定
- 每次请求变更保存一次,画布就绪时——不是在每次微编辑后。
- 用户要草稿时,结尾说明"草稿已就绪,可预览并 promote";草稿 → 构建 → 预览 → promote 流程由
validating-and-publishing-canvases覆盖。 - 回复结尾必须给出画布所在频道并附上链接——使用画布工具返回的
url字段(canvas-create、canvas-list与发布/源响应都携带它)。该字段是画布唯一合法链接,永远不要自行构造:猜测的 URL(项目页、web 路由)无法解析。
小结
building-canvases技能把自由画布开发压缩成一条可验证的闭环:解析目标(不猜测频道、不造重复品)→ 按请求形态加载实现与校验技能 → 以平台契约约束的项目形状编写源码 → 用无副作用的canvas-validate-create洗掉全部 error 诊断 → 用expected_current_version_id守卫发布(或按需暂存草稿)→ 轮询构建到终态。其背后是一套相当严格的平台工程:append-only 的源版本序列与 last-good 构建指针保证失败发布不污染在线画布;能力声明 + 运行时强制 + CSP 注入构成最小权限边界;64 KB / 256 键的 state 限制与 64 文件 / 2 MB 的源码限制让每次访问保持点查、每次构建保持有界。理解这条"文档规则—校验诊断—后端模型"三层一致的链条,是可靠地为 PostHog 构建、更新和修复独立画布应用的关键。
【免费下载链接】posthog:hedgehog: PostHog is the leading platform for building self-driving products. Our developer tools – AI observability, analytics, session replay, flags, experiments, error tracking, logs, and more – capture all the context agents need to diagnose problems, uncover opportunities, and ship fixes. Steer it all from Slack, web, desktop, or the MCP.项目地址: https://gitcode.com/GitHub_Trending/po/posthog
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考