Twenty Apps 开发指南:hello-world 示例中 LLM 协作开发的三条核心规则(UUID v4、视图导航关联、组件响应式)
本文以 Twenty 官方 hello-world 应用示例中的 LLM 指令文档 LLMS.md 为主体,逐条拆解其中定义的"UUID 必须为 v4"硬性要求与两条常见陷阱,并对照该示例应用的全部源码实体(对象、视图、导航菜单项、前端组件、逻辑函数)说明每条规则背后的工程约束。读完后,你应当掌握用 LLM 辅助生成 Twenty App 代码时的三条自检规则,以及如何在仓库中找到对应的参照实现与更完整的 rich-app 样例。
一、LLMS.md 的定位:写给 LLM 的应用开发守则
hello-world 是 Twenty 仓库内置的"最小可用"应用示例,其 README 中明确写道:"Main docs and pitfalls are available in LLMS.md file."(见 README.md)。也就是说,LLMS.md 是该应用面向大语言模型(LLM)的官方提示文档,用于在 LLM 生成或修改应用代码时约束其行为边界。该文档由三个部分组成:
- Base documentation(基础文档入口):指向 Twenty 官方应用开发 Getting Started 文档,并指向一个内容更丰富的完整应用样例——在本仓库中对应 rich-app 示例,当 hello-world 不足以覆盖某种模式时,rich-app 是首选参照;
- UUID requirement(UUID 要求):"All generated UUIDs must be valid UUID v4."(所有生成的 UUID 必须是合法的 UUID v4);
- Common Pitfalls(常见陷阱):两条具体陷阱——
- "Creating a view without a navigationMenuItem associated."(创建视图时未关联对应的 navigationMenuItem);
- "Creating a front-end component that has a scroll instead of being responsive to its fixed widget height and width, unless it is specifically meant to be used in a canvas tab."(前端组件内置了滚动,而不是响应式地适应其固定的 widget 高度与宽度,除非该组件专门用于 canvas tab)。
这三条规则看似简短,却分别对应 Twenty 应用开发中三个最容易由 LLM"想当然"写错的层面:稳定标识符、界面导航注册、组件尺寸约束。下文逐条展开。
二、规则一:所有生成的 UUID 必须是合法的 UUID v4
2.1 为什么 universalIdentifier 是应用的核心
从 hello-world 示例的源码结构看,应用中的每一类实体都通过 universalIdentifier 字段建立唯一身份,且该值是硬编码在源码中的字符串常量,而不是运行时生成:
| 实体类型 | 定义文件 | universalIdentifier 示例 |
|---|---|---|
| 应用 | application-config.ts | bb1decf6-dee5-43ef-b881-9799f97b02a8 |
| 对象(Object) | example-object.ts | 47fd9bd9-392b-4d9f-9091-9a91b1edf519 |
| 字段(Field) | example-object.ts | 2d9ff841-cf8e-44ec-ad8e-468455f7eebd |
| 视图(View) | example-view.ts | 965e3776-b966-4be8-83f7-6cd3bce5e1bd |
| 导航菜单项 | example-navigation-menu-item.ts | 9327db91-afa1-41b6-bd9d-2b51a26efb4c |
| 前端组件 | hello-world.tsx | 7a758f23-5e7d-497d-98c9-7ca8d6c085b0 |
| 逻辑函数 | hello-world.ts | b05e4b30-72d4-4d7f-8091-32e037b601da |
| 安装前钩子 | pre-install.ts | f8ad4b09-6a12-4b12-a52a-3472d3a78dc7 |
| 安装后钩子 | post-install.ts | 8c726dcc-1709-4eac-aa8b-f99960a9ec1b |
这些标识符的作用可以从源码中相互引用的方式得到印证:example-object.ts 将对象标识符导出为 EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER、将 name 字段导出为 NAME_FIELD_UNIVERSAL_IDENTIFIER,随后 example-view.ts 通过 objectUniversalIdentifier 属性引用前者、通过 fieldMetadataUniversalIdentifier 引用后者,example-navigation-menu-item.ts 再引用视图的标识符。可见 universalIdentifier 是跨实体装配应用的稳定主键:安装、同步、升级都依赖它识别"这是同一个实体"。因此 LLM 新增任何实体时,必须生成一个全新的、合法的标识符并硬编码导出,而不能省略或随意填写。
2.2 如何验证一个 UUID 是合法的 v4
UUID v4 的格式为 8-4-4-4-12 共 32 位十六进制数,其中有两个"指纹位":
- 第三组首位必须是版本位
4; - 第四组首位必须是变体位,取值范围为
8、9、a、b。
以本示例中的应用标识符 bb1decf6-dee5-43ef-b881-9799f97b02a8 为例:第三组 43ef 以 4 开头(版本位正确),第四组 9799 以 9 开头(变体位合法),因此是一个标准的 UUID v4。示例中列出的其余标识符(如 47fd9bd9-392b-4d9f-9091-9a91b1edf519、965e3776-b966-4be8-83f7-6cd3bce5e1bd)同样满足这两条指纹规则。
这对 LLM 生成代码有直接的实操含义:
- 生成的每个
universalIdentifier都应先通过上述两条指纹位校验; - 标识符必须在整个应用内唯一,新增实体时不能复制已有实体的值;
- 标识符一经写入源码即成为稳定契约,后续迭代不应重新生成。
三、规则二:视图必须关联 navigationMenuItem,否则不会按预期出现在左侧边栏
LLMS.md 的第一条 Common Pitfall 指出:"Creating a view without a navigationMenuItem associated." 即只创建视图而不创建关联的导航菜单项,是 LLM 生成应用代码时最常见的错误之一。hello-world 示例给出了正确的完整配对方式。
3.1 视图侧:只负责声明"看什么"
example-view.ts 通过 defineView 声明了一个针对自定义对象 exampleItems 的列表视图:
export const EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER = '965e3776-b966-4be8-83f7-6cd3bce5e1bd';
export default defineView({
universalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
name: 'All example items',
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
icon: 'IconList',
position: 0,
fields: [
{
universalIdentifier: 'f926bdb7-6af7-4683-9a09-adbca56c29f0',
fieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
position: 0,
isVisible: true,
size: 200,
},
],
});
注意视图中每一列同样拥有自己的 universalIdentifier(这里是 name 字段列),并引用对象字段定义中的 NAME_FIELD_UNIVERSAL_IDENTIFIER——"每列一个 v4 UUID"的规则在视图中再次得到贯彻。
3.2 菜单项侧:负责把视图"挂"到左侧导航
example-navigation-menu-item.ts 通过 defineNavigationMenuItem 声明导航入口,并以 type: NavigationMenuItemType.VIEW(类型来自 twenty-shared/types)加 viewUniversalIdentifier 指向该视图:
export default defineNavigationMenuItem({
universalIdentifier: '9327db91-afa1-41b6-bd9d-2b51a26efb4c',
name: 'example-navigation-menu-item',
icon: 'IconList',
color: 'blue',
position: 0,
type: NavigationMenuItemType.VIEW,
viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
});
从源码结构看,两者之间不互相导入组件实现,而是通过视图导出的标识符常量完成装配:菜单项决定"左侧边栏出现什么、排第几、什么颜色图标",视图决定"点开之后展示哪些字段"。因此 LLM 在生成代码时的检查项很明确:
- 每生成一个
defineView,必须同时生成一个defineNavigationMenuItem; - 菜单项的
viewUniversalIdentifier必须引用视图导出的标识符常量,而不是手敲一份副本; - 菜单项自身同样需要独立的 UUID v4 标识符。
四、规则三:前端组件必须响应式适应 widget 尺寸,默认不内置滚动
第二条 Common Pitfall 要求:前端组件不应自带滚动条(scroll),而应响应式地适应其所在的 widget 的固定高度与宽度——唯一的例外是该组件明确用于 canvas tab 场景。
4.1 组件运行环境决定了这条规则
应用的前端组件最终由 Twenty 的前端组件渲染器加载执行,相关运行时实现位于 twenty-front-component-renderer 包中:从源码结构看,它包含 host/(宿主侧)与 remote/(远端沙箱侧)两部分及 polyfills/ 目录,负责把应用包中的 React 组件挂载到 Twenty 界面里的具体插槽中。组件在记录页、看板等位置呈现时,通常被放进尺寸固定的 widget 容器——如果组件内部再声明一套自己的滚动区域,就会出现"滚动套滚动"、内容被双重裁剪的观感问题。LLMS.md 因此把"响应容器而非自滚"定为默认规范,仅当组件是专为 canvas tab(大画布场景,用户预期内容可滚动扩展)设计时,才允许内部滚动。
4.2 hello-world 组件的示范写法
hello-world.tsx 展示了符合该规则的典型形态:
const client = new CoreApiClient();
// ...
return (
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
<h1>Hello, World!</h1>
<p>This is your first front component.</p>
{data ? (
<div>
<p>Company name: {data.name}</p>
<p>Company id: {data.id}</p>
</div>
) : (
<p>Company not found</p>
)}
</div>
);
export default defineFrontComponent({
universalIdentifier: HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
name: 'hello-world-front-component',
description: 'A sample front component',
component: HelloWorld,
});
要点有三:
- 组件根节点是普通流式
div,不设固定高度、不设overflow滚动,内容自然撑开或由父容器约束; - 组件通过
CoreApiClient(来自twenty-client-sdk/core)发起类型化的 GraphQL 查询(示例中按position: 1过滤取一条 company 记录),展示应用组件与宿主数据模型的交互方式; - 组件通过
defineFrontComponent注册,并遵循规则一使用 UUID v4 的universalIdentifier。
对 LLM 的落地约束可以归纳为:生成组件时避免 height: 100vh、固定像素高容器、overflow: auto/scroll 这类写法;除非需求明确说明组件用于 canvas tab,否则一律按"内容自适应、滚动交给宿主"来写。
五、配套工作流与落地检查清单
规则生效的场景,正是 README 中描述的 LLM 参与开发的工作流(见 README.md):
# 认证到目标 workspace
yarn twenty remote:add --api-url http://localhost:2020 --as local
# 启动开发模式(watch + build + sync + 自动生成类型化 client)
yarn twenty dev
# 用脚手架生成新实体(object / field / function / front-component / role / view / navigation-menu-item)
yarn twenty dev:add
yarn twenty dev:add 支持脚手架化的实体类型中同时包含 view 与 navigation-menu-item,这正对应本文规则二强调的"成对生成"。综合 LLMS.md 与示例源码,LLM 生成或修改 Twenty App 代码后,建议按下述清单自检:
- 标识符:所有新增/修改的
universalIdentifier(应用、对象、字段、视图、视图列、菜单项、组件、逻辑函数)均为合法 UUID v4,且全应用唯一; - 视图-导航配对:每个新视图都有对应的
defineNavigationMenuItem,且通过导出的标识符常量引用该视图; - 组件响应式:前端组件不内置滚动,适应 widget 固定尺寸;仅 canvas tab 专用组件例外;
- 引用方式:实体间引用一律使用导出的标识符常量,不复制粘贴字符串;
- 模式不足时:hello-world 覆盖不到的更复杂模式(角色权限、技能、字段类型等),参考本仓库中更完整的 rich-app 样例 与 LLMS.md 所指向的官方 Getting Started 文档。
以上五条规则在 hello-world 示例的每一个源码文件中都有对应实例可循,是 LLM 与开发者协作开发 Twenty 应用时最基础、也最值得固化的行为约束。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00