Sim 块可见性门控实战:用 preview 标志、AppConfig 与 PREVIEW_BLOCKS 管理 Block 灰度发布与一键下架
【免费下载链接】simSim is the collaborative workspace to build, deploy, and monitor AI agents and workflows. Used by 100,000+ builders.项目地址: https://gitcode.com/GitHub_Trending/sim16/sim
Sim 的块(Block)是工作流画布上的核心构建单元,而"块可见性门控"(block visibility gating)决定了某个块能出现在哪些发现面(Discovery Surface)上。本文以仓库中.agents/skills/add-block-preview/SKILL.md为骨架,结合 block-visibility.ts、registry.ts 与 visibility 目录下的实现与测试,系统讲解如何将一个未发布的块以 Preview 形态灰度放出、按组织/用户白名单定向开放、最终 GA,以及如何对已上线的块执行 Kill Switch 应急下架——同时保证"执行永不门控"这一核心不变量。
门控模型:三根杠杆,一个共享判定谓词
块可见性门控的全部语义收敛在三根杠杆上,求值逻辑集中在 apps/sim/lib/core/config/block-visibility.ts,最终投影通过 apps/sim/blocks/registry.ts 的注册表访问器对外暴露:
preview: true(BlockConfig 静态字段,写在代码里)——块默认在所有环境(hosted、self-hosted、本地 dev、SSR)隐藏,直到被显式揭示。这是**默认关闭(fail-closed)**的设计:没有揭示动作,预览块就永远不可见。- hosted 侧的
block-visibilityAppConfig 文档——按现有块类型(block type)为键的逐块规则,运行时可热更新,无需代码部署。 PREVIEW_BLOCKS环境变量(逗号分隔的块类型列表)——self-host 部署和本地开发绕过 AppConfig 的揭示通道。
三条路径求值后产出一个面向单个查看者(per-viewer)的投影,即BlockVisibilityState:
export interface BlockVisibilityState { revealed: Set<string> // 该查看者可见的 preview 块类型 disabled: Set<string> // 规则存在但未命中任何子句的类型(kill switch 命中非 preview 块) previewTagged: Set<string> // 已揭示但未全局 GA(enabled !== true)的类型,注册表会给它追加 " (Preview)" 后缀 }正如源码注释所说,三个集合缺一不可:revealed \ previewTagged就是"已通过配置 GA 但代码里preview: true尚未删除"的过渡窗口,而disabled命中的是与 preview 完全不相交的另一类群体(已上线的正式块)。
从 block-visibility.ts 的实现可以看到求值的完整逻辑:
- 当
isAppConfigEnabled为假(即非 hosted 环境),直接走getPreviewBlocksFromEnv()解析PREVIEW_BLOCKS,返回{ revealed, disabled: new Set(), previewTagged: new Set(revealed) }——非 hosted 环境下不存在 kill switch,也没有任何块被 disable。 - 当 AppConfig 可用时,读取
block-visibilityprofile(BLOCK_VISIBILITY_PROFILE = 'block-visibility'),逐条规则用matchesRule与查看者上下文匹配:命中则加入revealed(若enabled !== true同时加入previewTagged),未命中则加入disabled。 - 平台管理员状态懒解析且每次调用最多一次:只有当存在
adminEnabled规则且调用方没有自带ctx.isAdmin时,才通过动态 import 的isPlatformAdmin(super-user.ts)查询一次,避免把@sim/db拉进配置模块的加载图。
一个关键过滤:parseVisibilityConfig会丢弃所有custom_block_*前缀的键(block-visibility.ts)。因为自定义块(deploy-as-block)是组织作用域、由自己的 enabled/disabled 生命周期管理的,可见性文档绝不能门控它们。
三根杠杆对应的规则文档结构
hosted 的block-visibilityAppConfig 文档(profile 名block-visibility,对应基础设施栈中的BLOCK_VISIBILITY_PROFILE_NAME)按块类型为键,每条规则包含以下字段:
{ "<block-type>": { "enabled": false, // 必填。true = GA(对所有人可见) "orgIds": ["org_..."], // 可选白名单子句(任一命中即揭示) "userIds": ["user_..."], // 可选白名单子句 "adminEnabled": true // 平台管理员(user.role === 'admin')可见 } }判定语义在 block-visibility.test.ts 中有完整的测试覆盖,值得逐条对照:
- GA 规则:
{ "enabled": true }揭示且不带 preview 标签(previewTagged不含该类型)。 - 白名单规则:
{ "enabled": false, "orgIds": ["o1"], "userIds": ["u9"] }对命中 org 或 user 的查看者揭示并打 preview 标签;未命中的查看者进入disabled。 - Kill switch:
{ "enabled": false }(无任何白名单)对所有人禁用。 - 自定义块豁免:
custom_block_abc123键被解析阶段直接丢弃,永远不会被门控。 - 畸形条目:
{ "a": "nope" }这类非法条目被丢弃,不会误伤。
" (Preview)" 后缀与纯函数 getBlock
一个被揭示但未全局 GA 的块(enabled !== true,或仅由 env 揭示)会在所有发现面上渲染为带" (Preview)"名称后缀的形式——这是视觉上区分"内测中"与"正式版"的关键信号。
同时,getBlock() 保持纯函数:它只做注册表键查找(含连字符归一化与自定义块 overlay 回退),绝不应用可见性投影。因此画布上已经放置的实例始终保留规范名称并永远可以执行。测试 visibility.test.ts 专门断言了这一点:即使某块同时处于revealed、previewTagged和disabled状态,getBlock('gmail_v2')?.name仍是GMAIL_V2,getBlock('slack')?.hideFromToolbar仍是undefined。
预览块的生命周期:从 Author 到 GA 的完整流程
SKILL.md 给出了一条可落地的操作链路,每一步都有源码佐证:
1. 正常编写块并标记 preview。用/add-block等方式正常编写块,在BlockConfig上设置preview: true。在 GA 之前不要发布BlockMeta,也不要生成文档。
为什么?仓库中有两道校验配合:
check-block-registry脚本会刻意跳过 preview 块的 meta 覆盖检查;- generate-docs.ts 在每一道生成关口都跳过 preview 块——源码里预览门控是
return /preview\s*:\s*true/.test(blockContent)(generate-docs.ts),注释明确写着"THE single preview gate for this script——它产出的每一个面都以此为准"。预览块不会进入任何公开面,包括 icon map(Unreleased preview blocks never reach any public surface, icon map included.),甚至只被 preview 块托管的触发器(trigger)也会被collectPreviewOnlyTriggerIds()收集后一并跳过(generate-docs.ts)。
2. 本地开发。在环境变量中设置PREVIEW_BLOCKS=<block-type>即可看到该块(带 " (Preview)" 后缀)。getPreviewBlocksFromEnv()(env-flags.ts)对逗号分隔的条目做 trim 后去空,块类型本身是 snake_case 小写,因此不额外转小写。注意:这个环境变量在 hosted 上不生效,它是isAppConfigEnabled为假时的专属通道。
3. 合并 / 部署。块的代码在所有环境都生效了,但任何地方都不可见——没有 AppConfig 规则,self-host 也没有 env 条目。这正是"代码已上线、可见性为零"的 fail-closed 状态。
4. Hosted 预览。向block-visibilityAppConfig 文档添加规则并发起部署(无需代码部署):
- 仅管理员可见:
{ "enabled": false, "adminEnabled": true } - 设计合作伙伴组织可见:
{ "enabled": false, "orgIds": ["org_123"] } - 通过配置 GA(代码清理待办):
{ "enabled": true }——约 30 秒内(AppConfig TTL)后缀在所有地方消失,配合客户端重新拉取。
这条操作手册与feature-flags完全一致:编辑 hosted 文档后,用aws appconfig start-deployment以sim-<env>-fast策略发布(详见基础设施 README)。实现侧,block-visibility.ts 通过fetchAppConfigProfile携带{ application, environment, profile: 'block-visibility' }读取该 profile,命中测试断言(block-visibility.test.ts)也证实了这个调用形状。
5. GA 清理。删除块上的preview: true(现在 self-host 用户升级后也能看到了),添加BlockMeta并重新生成文档,同时删掉 AppConfig 条目。如果是v2 升级,这一时刻也正是给 v1 打上hideFromToolbar: true以及sunset: { status: 'legacy', replacedBy: '<v2-type>' }的时机(被替代版本的范式)。
这里有一个必须在同一 commit 落地的约束:preview: true的删除与 v1 的sunset标记必须同一次提交,因为check-block-registry会拒绝replacedBy仍指向 preview 块的 sunset 块——拆成两次提交会在中间态把构建搞挂。
还有一个容易踩的坑:需要把块的BLOCK_DISPLAY_WORKFLOWS条目(apps/docs/components/workflow-preview/block-display-workflows.ts)迁移到新类型,否则文档页上的<BlockPreview>会静默渲染空白。这个文件是块参考文档 hero 区的"单块预览工作流"源数据,每个条目与画布上呈现的内容严格一一对应,是BlockPreview的唯一数据来源。
Kill Switch:已上线块的应急下架
当已 GA 的块需要从发现面撤下时(事故、弃用),在文档中添加{ "<block-type>": { "enabled": false } }即可。白名单子句可以开出例外(比如orgIds/userIds继续可见)。
关键边界:执行不会停止。已经在使用该块的工作流继续正常运行;kill switch 只阻止新的放置与发现。从 context.ts 的isHiddenUnder可以看到,disabled命中时块被隐藏,但执行路径根本不经过这个谓词。
不可违反的不变量
SKILL.md 明确列出了门控系统必须遵守的六条不变量,每一条都能在源码里找到对应实现:
1. 执行永不被门控。executor、serializer、drop-naming 以及isBlockTypeAccessControlExempt都通过纯函数getBlock解析。禁止在任何执行路径上添加可见性检查。这由 registry.ts 的纯getBlock和 server-context.ts 的设计共同保证——执行入口(execute 路由、trigger.dev 任务、schedules/webhooks)从不建立可见性作用域,已放置的 preview 块永远能序列化并运行。
2. 克隆而非移除(Clone-not-remove)。被门控的块仍然保留在getAllBlocks()输出中,只是以hideFromToolbar: true的浅克隆形式存在(registry.ts 的projectBlock)。依赖.find-by-type 的消费者依赖这一点,绝不能把它们 filter 掉。测试 visibility.test.ts 验证了"无上下文时 preview 块仍存在于getAllBlocks()且被标记为隐藏,而底层注册表条目未被改动"。
3. 键必须是注册表块类型。绝不能用custom_block_*——解析阶段会丢弃它们(自定义块有自己的 enabled/disabled 生命周期)。
4. 共享隐藏谓词是isHiddenUnder。所有发现面(注册表投影、VFS stamp 过滤、exposed-integration-tools 过滤、get_blocks_metadata)都必须调用它,禁止在新消费点内联重写 preview/disabled 规则。isHiddenUnder的语义(context.ts):未揭示的 preview 块即使状态为 null 也隐藏(fail-closed),kill switch 命中的类型只在有 active 状态时隐藏。注意静态的hideFromToolbar刻意不属于该谓词,需要它的调用方单独检查——这正是 registry.tseffectiveHidden的职责。
5. 进程级缓存保持不门控。getStaticComponentFiles(VFS)和getExposedIntegrationTools构建的是"未门控全集";按查看者过滤发生在 stamp/消费时刻。禁止把门控移进共享 builder。相应地,context.ts 提供了registerBlockCacheInvalidator/invalidateBlockCaches机制——客户端可见性状态变化时,非 React 模块缓存(tool-operations 搜索索引、integration matcher)通过注册的回调统一重置。
6. 门控是"表面隐藏"而非"保密"。完整配置会随客户端 JS bundle 下发,任何真正机密的东西都不能做成注册块。
上下文机制的实现细节
isHiddenUnder所在的 context.ts 是一个同构模块(无'use client'、无node:导入),通过registerBlockVisibilityResolver注册环境特定的解析器:
- 客户端:client.ts 在模块加载时即以
null状态注册,保证首次渲染(包括 SSR pass)对 preview 块 fail-closed;挂载后的拉取只会揭示(良性 pop-in)或对已公开块应用 kill switch。hydrateBlockVisibility还做了深比较去抖——React Query 轮询会返回内容相同的新对象,若不去重,每次轮询都会重建工具栏、搜索、matcher 缓存。工作区切换时resetBlockVisibilityForSwitch会立即丢弃 preview 揭示(可能不适用于新工作区),但保留 kill-switch 条目直到新投影到达,避免禁用块在切换飞行窗口内闪回发现面。 - 服务端:server-context.ts 用独立的
AsyncLocalStorage做每请求作用域,与自定义块 overlay 的 ALS 互不干扰,withBlockVisibility与withCustomBlockOverlay可以任意顺序嵌套。
测试防线
门控行为的两层测试是仓库中需要重点维护的防线:
- 求值语义:apps/sim/lib/core/config/block-visibility.test.ts——覆盖 env 回退、AppConfig profile 拉取、GA/白名单/kill-switch 规则、
custom_block_*丢弃、畸形条目丢弃,以及 admin 每调用最多解析一次。测试用setEnvFlags({ isAppConfigEnabled: true })强制走 AppConfig 路径,并用mockFetch.mockImplementation((_ids, parse) => Promise.resolve(parse(doc)))模拟本地 AppConfig 文档。 - 注册表投影:apps/sim/blocks/visibility/visibility.test.ts——验证克隆而非移除、preview 后缀、config-GA 无后缀、kill switch 仅在 active 上下文生效、
getBlock保持纯净、未受影响块返回原引用、以及已隐藏自定义块不被二次克隆或加后缀。
当门控行为发生变化时,扩展这两个文件:admin 子句用mockIsPlatformAdmin模拟,AppConfig 路径用本地withAppConfigharness。测试还印证了isAppConfigEnabled的判断(env-flags.ts):只有 hosted 且同时配置了APPCONFIG_APPLICATION与APPCONFIG_ENVIRONMENT时才为真,self-host/OSS 部署永远走 env 回退,AppConfig 客户端在非 hosted 环境永远不会被触达。
结语:一条从代码到配置的灰度发布闭环
Sim 的块可见性门控把"发布"这件事拆成了代码部署与可见性配置两个独立维度:代码可以先进主干(preview: true兜底),可见性则通过 AppConfig 文档或PREVIEW_BLOCKS环境变量在运行时精确控制。无论面向管理员、设计伙伴组织还是全体用户,无论是新块灰度还是已上线块应急下架,都遵循同一套三杠杆模型与六条不变量。理解这套机制,你就能在 Sim 中安全地管理任意块的发布节奏——从首个 preview 提交到最终 GA 清理,全程不影响已放置实例的执行。
【免费下载链接】simSim is the collaborative workspace to build, deploy, and monitor AI agents and workflows. Used by 100,000+ builders.项目地址: https://gitcode.com/GitHub_Trending/sim16/sim
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考