作者指南:让积木卡片用一句人话说话)
Sim 工作流画布句子Canvas Sentences作者指南让积木卡片用一句人话说话【免费下载链接】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默认以标签/值行label/value rows展示其配置。Canvas Sentences画布句子允许开发者用一段散文替换这些行——例如把 Slack 积木的卡片从多行表单渲染成一行Post ⟨Ship it ⟩ to ⟨#eng⟩其中⟨…⟩是实时填充的值芯片。本文以 apps/sim/blocks/AGENTS.md 为骨架结合仓库中校验器、解析器与真实积木源码完整讲解如何为积木作者声明、验证画布句子并规避那些静默破坏卡片的经典陷阱。读完你将掌握canvasPresentation.sentences的完整 DSL、core语义、触发卡片句式以及check-canvas-sentences.ts与canvas-sentence-spec.ts两个命令行工具的正确用法。一、前置约束Blocks 作用域规则画布句子不是孤立功能它依附于积木定义本身。在编写任何句子之前apps/sim/blocks/AGENTS.md 首先为apps/sim/blocks/**下的所有积木定义划定了硬性边界这些规则与句子直接相关type值与工具映射必须对齐真实集成工具 ID——句子声称的行为必须与工具实际执行的行为一致每个 subblock 的id在块内必须唯一即使在不同condition下也不得复用审慎使用condition、required、dependsOn、mode以反映 UX 与执行需求canonicalParamId只能用于关联同一逻辑参数的不同输入形态不得当作 subblockid复用canonical 组中若一个字段必填则组内所有备选字段也必须必填类型转换放在tools.config.params绝不能放在tools.config.tool支持文件输入时遵循 basic/advanced 双形态模式并用normalizeFileInput归一化块的输出必须与底层工具实际返回一致{Service}BlockMeta.skills必须源自tools.access暴露的真实操作与网上真实高频用例禁止编造hallucinate技能。这些约束中canonicalParamId、condition、mode三点会在后文反复出现——它们正是画布句子四大静默错误中的两个的来源。二、什么是画布句子用一句散文替换标签行一个积木的卡片可以将其标签/值行替换为一行散文。声明位置在canvasPresentation.sentencesdefault块没有操作下拉框operation dropdown时使用byOperation按下拉框的**选项 idoption ids**为键时使用。未声明任何句子的块保持行布局不变因此该机制的采纳是增量式的——存量积木不受影响。为什么需要校验一句话如果违反了规则运行期会静默失败不抛异常、不写日志只是卡片不再像画布上其他卡片那样读起来像一句话。这正是check-canvas-sentences校验器存在的根本原因。校验命令作用于单个积木便于作者自查bun run apps/sim/scripts/check-canvas-sentences.ts --blocktype从源码看check-canvas-sentences.ts 还支持另外两个开关--require-coverage把覆盖率报告从进度条升级为硬性失败在全量上线完成前不要开启--render逐条打印每个句子在卡片上的真实阅读效果——在大规模fleet体量下这是唯一可行的文案审阅方式。校验器的核心思路不是写一个近似模型而是用真实解析器跑一张用户第一眼看到的空卡片。该文件注释中记录了一个惨痛教训模型此前报告所有句子健康实际却有 3,146 / 4,583 张卡片什么也没画出来早期版本校验每个 subblock 都在卡上比任何真实卡片都更宽松于是重新复述了模型自身的盲区放行了 35 个画不出任何内容的句子。现在它改用getCardSubBlocks画布自身过滤用的同一函数构建卡上字段集basic 模式、除 operation 外无任何值。三、core语义一张未配置卡片怎么说话默认情况下每个分句clause都是可选的——只有对应字段被填充后才渲染。core: true是唯一的例外该分句总是渲染字段有值时显示值无值时显示该字段的名词nounPost ⟨Ship it ⟩ to ⟨#eng⟩ ← 已配置 Post ⟨a message⟩ ← 未配置仅由 core 提供名词从 subblock 的title派生resolveFieldNoun因此作者零成本获得占位文案——但名词自带冠词a/an/the这就是为什么你绝不能在 core 芯片前再写冠词。当某个标题读进句中不自然时如 Message ID to Reply To请在 subblock 上设置canvasNoun覆盖而不是在文案里绕弯。派生名词的实际逻辑在 canvas-sentence-noun.ts它维护ID/URL/API/SQL等初始ism 集合保持大写、处理URL/UUID等元音字母但辅音开头的发音规则、识别hour/honest等辅音字母但元音开头的单词还处理单复数radius/status这类-us/-is结尾的单数词、不可数名词audio/code/content/media/text、表单脚手架括号Radius (meters) → a radius、选择器祈使前缀Select Channel → channel以及驼峰拆分SalesOrderType→ sales order type。围绕core有两条被强制校验的规则仅靠 core 分句就必须读起来是完整句子——它们是全新卡片展示的全部内容。校验器对空卡片运行真实解析器若解析结果为空即失败。因此每句话至少要有一个 core 分句或一段字面文案literal copy。core 分句的字段必须被证明在卡上针对该操作。这个 DSL 没有替补文案replacement copy被条件门控挡掉的分句会静默消失core做出的承诺就是假的。锚定一个该操作总是显示的字段或者去掉core让字面文案承担句子。同时不要把所有分句都标成core——一张配置完毕的卡片会变成占位符之墙。只标记句子真正关于的部分其余细节保持可选。3.1mode: advanced字段永远不能是core卡片默认以basic模式打开因此独立的mode: advanced字段根本不在卡上。对它标core就是做出卡片无法兑现的承诺分句消失而全新卡片也没有已填充的行最终积木只画出一个光秃秃的头部。文档明确指出这是最常见的出错方式——曾有 35 个句子踩中此坑如今被check:canvas-sentences捕获。校验器之所以能捕获是因为 canvas-sentence-validation.ts 的isFieldGuaranteed专门按 basic 视图用户落地的视图求值canonical 对中只有 basic 成员在 basic 卡片上mode: advanced的独立字段直接判定不可保证。注释里写得很直白这个模块曾经完全忽略 basic/advanced 模式因此认证了 35 个画不出任何内容的句子。此坑在list 操作上杀伤力最大——这类操作的字段通常只有高级分页控件它们需要的是字面文案而非锚点// ✗ limit 是 mode:advanced——全新卡片什么都渲染不出来 list_customers: [{ text: List customers, up to, field: limit, core: true }] // ✓ 字面文案总是渲染细节在填充后出现 list_customers: [List customers, { text: , up to, field: limit }]而一个 canonical basic/advanced配对则完全可以作为锚点——其中一个成员总在显示——前提是分句要列出每个成员field: [tableSelector, manualTableId]这一点被单独强制。3.2 空态即作用于全部的操作有些操作带 id 时作用于单条记录不带 id 时作用于全部记录——其 subblock 文案通常会说Leave empty to list all contacts。若把 id 标成core未配置的卡片就会断言一个它实际不会作用的单一目标。此时应写对两种状态都为真的字面文案// ✗ 声称读一个 contact未配置时实际返回所有 contacts get_contacts: [{ text: Read contact, field: contactId, core: true }] // ✓ 空态为真填充后也为真 get_contacts: [Read contacts, { text: matching, field: contactId }]3.3 给操作下拉框一个默认值如果块的 operation 下拉框没有value: () id创建时就会存入null于是没有任何byOperation条目匹配卡片在用户打开面板前一片空白。请用读操作而非变更操作作为用户最可能想要的默认。注意要用value:而不是defaultValue:——面板的下拉框只读value两者不一致会导致分歧。从 check-canvas-sentences.ts 的seededOperationValue可以看到这一约定的校验路径它会尝试执行subBlock.value({})获取下拉框种子值再用刚刚拖入、尚未操作的卡片状态asDropped跑一次paintsOnEmptyCard。四、触发卡片triggerSentences处于 trigger 模式的卡片绘制自己的句子来源是canvasPresentation.triggerSentencestriggerSentences?: { default?: CanvasSentence byTrigger?: Recordstring, CanvasSentence // 以 trigger id 为键 }绝大多数积木什么都不声明。触发模式通常只暴露 webhook URL 和签名密钥——是管道plumbing而非语义——而真正值得读的是选了哪个事件。这已被策展为apps/sim/triggers/中 trigger 自身的name因此卡片无需任何创作即可派生Run on Pull Request Opened。只有当配置本身就是语义时才声明triggerSentences例如 Schedule 的频率。句子随模式转变说的内容但语气不变动作句子说积木做什么触发句子说什么启动这次运行——Run on an email arriving in ⟨INBOX⟩绝不是Read an email。它是一个独立的插槽而非另一个byOperation键因为 trigger 模式会整体替换 subblock 集合而 operation 下拉框仍保留其动作模式默认值。Sim 自身的入口点start_trigger、manual_trigger、chat_trigger、api_trigger、input_trigger、starter豁免它们就是运行的开始头部已经说明再写句子只会重复。此豁免在 canvas-sentence-validation.ts 中以TRIGGER_SENTENCE_EXEMPT_TYPES集合实现同理condition、router_v2、note、starter四种动作卡片类型被SENTENCE_EXEMPT_TYPES排除在动作覆盖统计之外condition/router_v2画分支行、note根本不是积木卡片、starter是工作流入口而非步骤。五、语气Voice规则祈使句卡片说出自己的动作。Post a message to ⟨#eng⟩。绝不用第三人称Posts a message也绝不写Post a Slack message——头部已经写着 Slack再写一遍就是浪费这行空间。句子上方的标题是操作标题Send Email两者共享同一语气。check:canvas-sentences在动作卡片和触发卡片上都强制此规则。句子承载动词。它替换的是今天展示操作的芯片行。List channels而不是Channels。尽可能动词开头、值放最后——芯片是用户扫视的部分。一个分句一个事实最重要的在前。卡片宽 250px约两行就换行靠后的分句正是会溢出掉落的部分。最多三个值芯片两个读感最佳——每个芯片都增加宽度、把卡片撑高。没有值得展示字段的操作就是纯字面[List all channels]就是一条完整合法的句子。句子大小写、无结尾句号、能省则省的冠词Read schema of胜过Read the schema of the table。校验器如何强制祈使语气canvas-sentence-validation.ts 的checkImperativeVoice把句子首分句的文案经toImperativeLead来自 canvas-sentence-imperative.ts归一为祈使形态不一致即报错a card names an action, in the same voice as the operation title in its heading。它只作用于引导动词——句子后段协调的第二个动词Create or update record ⟨id⟩交给人工审查因为and/or在此处联结名词的情况远多于动词。六、结构Structure规则把句子关于的分句标core其余保持可选。一个句子可以按需拥有任意多个 core 分句——它们只占据自己的槽位没有只能锚一个的限制。当每个字段都是可选过滤器时用字面文案。list/search 操作的过滤器全部可选时没有任何东西值得占位。用字面文案开头、让每个分句都可选[List issues, { text: , assigned to, field: assignee }]。每个可选分句拥有自己的引导连接词。写{ text: , where, field: filter }绝不写{ ..., after: , where }, { field: filter }——掉落的分句会带走自己的text但前一个分句上的after会存活下来并悬空。裸字符串是常驻字面文案。优先把它们折进分句的text使它们能随值一起掉落。芯片是名词绝不是动词。芯片渲染的是下拉框的标签所以当标签本身是动词Archive/Unarchive时你不能围绕它构建句子——Apply ⟨Archive⟩ to thread ⟨id⟩就是错误示范。应陈述状态变化Set thread ⟨id⟩ to ⟨Archive⟩。悬空连接词问题在源码中是显式建模的DANGLING_CONNECTIVES集合收录了and、as、at、by、for、from、in、into、of、on、setting、to、using、where、with——任何存活下来的文案裸字符串或分句after都不能以这些词结尾否则卡片会渲染成Query rows from ⟨orders⟩, where后面什么都没有。七、静默破坏卡片的四大错误这是文档中最具实战价值的部分——四个错误全部无异常、无日志地静默发生只点名 canonical 对中的一个成员。一个canonicalParamId组包含一个 basic 选择器和一个 advanced 原始 id 输入advanced 模式用户只有后者被填充。必须列出每个成员field: [tableSelector, manualTableId]。当前仓库存在三种命名约定xSelector/x、xSelector/manualX、uploadX/xRef所以成员关系只能取自 spec 的canonicalGroups——无法从 canonical id 推导。field数组同样适用于互斥但非 canonical 对的备选——Slack 的目标是 channel或DM idSendblue 的是号码列表或群组 id。取第一个可用的。引用该操作从不显示的字段。被condition: { field: operation, value: [...] }门控的 subblock只能出现在它列出的操作的byOperation条目中。校验器对每个byOperation键执行visibilityForOperation若该 id 的每个定义在该操作下都判定hidden即报死配置dead config。两个操作渲染出相同的散文。在拥有数十个相似读端点的积木上很容易粘贴一个分句后从不调整——于是get_thread和get_thread_replies都说Read thread ⟨id⟩卡片无法告诉用户到底跑的是哪一个。要指名每个操作实际返回的内容。校验在两种读法上都执行因此两个操作即使可选分句清空后也不能碰撞——这正是未配置卡片的读法。Add tag ⟨id⟩ to contact ⟨x⟩和… to conversation ⟨y⟩清空后都剩下Add tag ⟨id⟩把目标分句标core才能区分它们。源码中groupByReading同时对filledReadings和bareReadings分组bare读法正是renderSentenceReadingscanvas-sentence-render.ts在可选分句全部为空时生成的。可选分句承载了动作的目标或范围。这一条校验器无法捕获——全库 895 个分句符合该形状而仅约 45 个是缺陷判别标准是语义的。问问自己这个分句消失后句子是什么意思Remove user ⟨id⟩读起来像删除用户而不是把用户移出群组Delete records from ⟨index⟩读起来像清空整个索引。如果截断后的读法指向更宽泛或不同的动作就把该分句标core让它站稳。Delete thread ⟨id⟩丢掉可选的, in ⟨channel⟩则完全没问题——意思不变。八、校验器在文案上强制的一条规则芯片前不得有冠词。对于core芯片名词已自带冠词所以Create a ⟨campaignType⟩会读成Create a a campaign type——去掉冠词让名词承载。对于可选芯片冠词必须与一个你无法预知其发音的下拉框标签一致Create a ⟨A/B Split⟩ campaign所以要把值从冠词后面移出来Create a campaign of type ⟨type⟩。校验器实现得相当精细core 情形直接拼出 …a a campaign type 报错可选情形则枚举下拉框每个选项的标签发音startsWithVowelSound找出与冠词不一致的选项。它甚至区分了发音是元音开头但字母是辅音one/URL/UUID与字母是元音但读作首字母缩写SMS读 ess-em-ess 所以用an两类特殊情况。关联词要么成对要么都不出现。以from …或between …开头的分句需要携带to …/and …的分句在空卡片上存活否则Route from ⟨an origin⟩描述的就是另一段旅程。两个半边都要标core。源码用CORRELATIVES常量建模{ opener: from, closer: to }与{ opener: between, closer: and }。校验器在句子中向后查找携带closer的分句若它可掉落则报错——注意它保护的是关系而非语法Route from ⟨origin⟩语法上完全成立只是语义被腰斩了。九、版本化积木继承何时是 bug28 个文件导出了_v2或_v3…变体通过展开基础对象实现——export const GmailV2Block { ...GmailBlock, type: gmail_v2, ... }。展开会连带携带canvasPresentation这只有在该变体拥有相同的 operation 下拉框时才是正确的。当操作集合不同时继承就是 bug。file.ts链式导出FileV5 → FileV4 → FileV3继承下来的byOperation携带了file_parser_v3之类的键——而后续下拉框中并没有这些选项——校验器会报no option with id。凡是下拉框不同的导出都必须给出自己的canvasPresentation。真实案例可从 slack.ts 末尾看到slack_v2先断言SlackBlock.canvasPresentation?.sentences?.byOperation必须已定义再展开基础canvasPresentation并用getSlackV2OperationSentences()生成的byOperation覆盖——因为它知道变体的操作集合已经不同。务必对文件导出的每个注册表类型都运行--blocktype而不只是基础类型。那才能告诉你处于哪种情形。十、完整工作示例文档给出一个可直接对照的声明canvasPresentation: { defaultTitle: Table, sentences: { byOperation: { query_rows: [ { text: Query rows from, field: [tableSelector, manualTableId], core: true }, { text: , where, field: [filterBuilder, filter] }, { text: , up to, field: limit, after: rows }, ], get_schema: [ { text: Read the schema of, field: [tableSelector, manualTableId], core: true }, ], }, }, }渲染效果已配置Query rows from ⟨orders⟩, where ⟨status open⟩, up to ⟨100⟩ rows只填表Query rows from ⟨orders⟩未配置Query rows from ⟨a table⟩注意三点细节canonical 对[tableSelector, manualTableId]两个成员全部列出可选的, where连接词属于自己的分句{ text: , where, field: ... }after: rows让限值芯片后跟一个名词。解析器 canvas-sentence.ts 的resolveCanvasSentence是纯函数编辑器画布、侧栏预览和文档画布为同一积木状态解析出相同句子只是每个界面各自渲染芯片的值编辑器通过 React Query 解析表名和凭据名预览则没有 hooks。十一、其他会咬人的细节defaultTitle是canvasPresentation类型的必填项所以声明句子也同时决定了自动命名实例的卡片头部。使用积木名去掉 (Legacy) 后缀的形式。不要在句子改动中顺带添加typeLabel或operationSubBlockId——那会改变已存在工作流的标题解析。触发模式没有句子。双模式积木的卡片在用作 trigger 时保持行布局所以只为动作侧编写。另外值得一提的是画布句子的换行估计estimateSentenceLines用近似字符宽度正文 6.3px/字符、芯片 6.8px/字符、芯片 26px 内边距估算句子会折成几行用于卡片高度预留。注释明确说它故意近似——差一行只影响预留高度卡片实际轮廓由其自身 DOM 测量决定。十二、创作上下文先跑 spec 工具创作时从 spec 工具开始——它能解析句子依赖的一切事实你无需从 500–3600 行的积木文件里重建bun run apps/sim/scripts/canvas-sentence-spec.ts --blockslack --prettycanvas-sentence-spec.ts 按操作报告标签、底层工具做什么does、以及fields——该操作能显示的唯一 subblock id 集合已穿透每个condition解析完毕另有canonicalGroups完整枚举每个配对成员。在操作fields列表之外命名字段是死分句 bug只列 canonical 组成员之一则是advanced 模式 bug——spec 让两者都变成机械操作。它还做了两件精细的事其一解析 subblock 可见性时同时处理condition的and分支一个以{ field: processingMode, value: async, and: { field: operation, value: analyze_id, not: true } }门控的 subblock 对analyze_id确实隐藏报为可见会引入校验器也无法捕获的死分句其二当 canonical 组任一成员不可用凭据选择器、密码字段时整组丢弃——逐成员过滤会留下manualCredential被推荐而credential被隐藏而只点名配对中一个成员恰是组列表要防止的 bug。spec 还排除了无用的字段类型oauth-input、credential-selector以及所有password: true的字段——句子永远不该命名这些操作选择器已提供动词凭据渲染为•••或掩码账号名。关于hasCatalogEntry为true时操作描述来自packages/deployment-config/src/integrations.json的生成目录为false时积木属于category: blocks在integrations.json中没有行——改为阅读apps/sim/tools/{name}/*.ts中每个操作的描述enrichment积木是唯一例外它的每操作散文存放在apps/sim/enrichments/{id}/{id}.ts的EnrichmentConfig.description上。spec 已经省略了凭据、密码和操作选择器因此它列出的任何内容都适合做成芯片。带默认值的下拉框仍是判断题——level默认为All时花一个芯片去说All等于什么都没说。当工具描述无法确定意图时apps/docs/content/docs/integrations/{service}.mdx带有手写的MANUAL-CONTENT-START:intro散文可供参考。结语把静默变成可检查画布句子的核心哲学是声明式文案 机械校验句子是纯数据CanvasSentence由裸字符串与{ text, field, after, core }分句组成解析器是纯函数校验器则用真实解析器、真实卡片可见性过滤与真实种子值在 PR 合入前就把卡片会画空这类运行时静默故障消灭掉。对积木作者而言正确的工作流是先跑canvas-sentence-spec.ts --blocktype --pretty拿到操作、字段与 canonical 组的权威事实再按本文的语气与结构规则写句子最后用check-canvas-sentences.ts --blocktype --render验证——通过--render亲眼确认每张卡片的真实读法。这套机制让 Sim 画布上数百个集成积木的卡片读起来像同一作者写就的一行行人话而不是一排排令人困惑的表单行。【免费下载链接】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),仅供参考