nocobase-dsl-reconciler

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

NocoBase Application Builder (DSL path)

NocoBase应用构建器(DSL路径)

Before you use this skill

使用本技能前须知

This is the opt-in DSL path. Default is
nocobase-ui-builder
. Stay on this skill only when the user explicitly wants YAML files they can commit +
cli push
. If you arrived here from a generic "build me a NocoBase app" request without the user naming DSL/YAML/git, switch to
nocobase-ui-builder
instead — it's the default entry point.
这是可选DSL路径,默认路径为
nocobase-ui-builder
。仅当用户明确需要可提交的YAML文件以及
cli push
功能时,才使用本技能。如果用户只是泛泛地提出“帮我构建一个NocoBase应用”,未提及DSL/YAML/git,请切换至
nocobase-ui-builder
——这是默认的入口路径。

Golden rule

黄金准则

templates/crm/
is a read-only reference library. When you're unsure how a specific field, block, or popup is shaped, open the closest CRM example, read it, then write your own adapted version in your workspace. Per-scenario pointers live in the workflow sections below.
Never copy CRM files wholesale into your workspace. Do not
cp -r templates/crm/...
to get started, do not duplicate
collections/nb_crm_*.yaml
, do not base your
routes.yaml
on CRM's. Bulk-copying drags unrelated leads/opportunities/orders state and workflows into your project — you then spend the whole session fighting hundreds of irrelevant validator errors instead of building your module.
The pre-deploy spec validator catches most structural mistakes with a clear error message. Trust the validator: when it errors, fix what it says rather than guessing — don't grep through
src/deploy/*.ts
.
templates/crm/
只读参考库。当你不确定某个特定字段、区块或弹窗的结构时,打开最接近的CRM示例,阅读后再在你的工作区编写适配版本。各场景的指引请见下文的工作流章节。
切勿将CRM文件整体复制到你的工作区。不要通过
cp -r templates/crm/...
来启动项目,不要复制
collections/nb_crm_*.yaml
,不要以CRM的
routes.yaml
为基础编写你的配置。批量复制会将无关的线索/商机/订单状态及工作流带入你的项目——你将花费大量时间处理数百个无关的验证器错误,而非构建你的模块。
预部署规范验证器会捕获大多数结构错误并给出清晰的错误信息。信任验证器:当出现错误时,按照提示修复,而非猜测——不要去查找
src/deploy/*.ts
的内容。

When NOT to use this skill

本技能的不适用场景

Hand off to the matching skill when the user's request is orthogonal:
User asks for…Skill
One-off live-UI tweak on an already-running page (move / reorder / reconfigure a single block, field, action) — no DSL commit wanted
nocobase-ui-builder
ACL / role permissions / route permissions
nocobase-acl-manage
Workflow create / update / revision / execution
nocobase-workflow-manage
(this skill only wires the trigger button; authoring the graph goes there)
Collection / field / relation authoring outside a DSL project
nocobase-data-modeling
Plugin development (
.tsx
components, server code)
nocobase-plugin-development
Install / enable plugin
nocobase-plugin-manage
Environment setup / app install / upgrade
nocobase-env-bootstrap
Any change that should live as a committed YAML file under
workspaces/<project>/
— stays here.
当用户的请求属于以下范畴时,请转交至对应的技能:
用户要求…对应技能
对已运行页面进行一次性实时UI调整(移动/重新排序/重新配置单个区块、字段、操作)——无需提交DSL
nocobase-ui-builder
ACL/角色权限/路由权限
nocobase-acl-manage
工作流创建/更新/修订/执行
nocobase-workflow-manage
(本技能仅负责配置触发按钮,工作流图的创作需使用该技能)
在DSL项目外进行集合/字段/关联关系创作
nocobase-data-modeling
插件开发(
.tsx
组件、服务端代码)
nocobase-plugin-development
安装/启用插件
nocobase-plugin-manage
环境搭建/应用安装/升级
nocobase-env-bootstrap
所有需要以提交的YAML文件形式存储在
workspaces/<project>/
下的变更——均保留在本技能中处理。

Environment

环境配置

bash
cd <skill-dir>/src
export NB_USER=admin@nocobase.com NB_PASSWORD=admin123 NB_URL=http://localhost:14000
bash
cd <skill-dir>/src
export NB_USER=admin@nocobase.com NB_PASSWORD=admin123 NB_URL=http://localhost:14000

Quick start — new build

快速开始——新建项目

Default path for "build me a NocoBase app": copy the starter and modify it. Do not hand-write the skeleton; do not study CRM first.
bash
cd <skill-dir>/src
cp -r ../templates/starter ../workspaces/<name>
“帮我构建一个NocoBase应用”的默认路径:复制启动模板并进行修改。不要手动编写骨架,也不要先研究CRM模板。
bash
cd <skill-dir>/src
cp -r ../templates/starter ../workspaces/<name>

First push: --copy bypasses validation rules that only matter once

首次推送:--copy 参数可绕过仅当弹窗存在时才生效的验证规则(如m2o弹窗绑定、clickToOpen文件存在性)。

popups exist (m2o popup binding, clickToOpen file presence). The

启动模板自带弹窗,因此首次推送实际上是完全有效的——--copy参数仅适用于你在弹窗配置完成前的扩展操作

starter ships with its own popups so the first push is actually fully

valid — --copy is for your extensions before popups are wired.

npx tsx cli/cli.ts push <name> --force

The starter is a complete minimal CRUD — 1 collection (Projects),
1 Dashboard page (4 KPI tiles + 2 charts), 1 list page with
filterForm / table / addNew popup / detail popup / 2 updateRecord
recordActions. Push as-is → visible in NB → then edit.
npx tsx cli/cli.ts push <name> --force

启动模板是一个完整的极简CRUD应用——包含1个集合(Projects)、1个仪表盘页面(4个KPI卡片+2个图表)、1个列表页面(含筛选表单/表格/新建弹窗/详情弹窗/2个更新记录的recordActions)。直接推送即可在NocoBase中查看,之后再进行编辑。

Customizing the starter (the agile loop)

自定义启动模板(敏捷迭代流程)

Iterate one concern at a time, push between each:
  1. Rename identifiers to match the user's domain.
    • collections/nb_starter_projects.yaml
      → your collection name (match the
      nb_<module>_<entity>
      convention)
    • routes.yaml
      : change "Starter" / "Projects" titles
    • pages/starter/
      directory name if you want (match
      routes.yaml
      key)
    • Find-replace
      nb_starter_projects
      across all files
  2. Adjust the field list in your collection YAML to match the user's entity. Update
    pages/<...>/layout.yaml
    fields:
    and the popup templates'
    field_layout:
    accordingly.
  3. Add a 2nd entity only when the first one works end-to-end. Create a new
    collections/*.yaml
    +
    pages/<module>/<entity>/
    dir, copy
    pages/starter/projects/layout.yaml
    as a starting point.
  4. Extend incrementally: add a tab, a chart, a workflow trigger. Push after every change. See "Advanced patterns" below for which CRM file matches which pattern.
Never write the whole module in one shot. For customer-facing builds — land the skeleton, show the user, gather feedback, iterate. The starter push takes minutes; a hand-built module takes hours.
每次只处理一个关注点,每次变更后推送:
  1. 重命名标识符以匹配用户的业务领域。
    • collections/nb_starter_projects.yaml
      → 你的集合名称(遵循
      nb_<module>_<entity>
      命名规范)
    • routes.yaml
      :修改“Starter”/“Projects”标题
    • 可修改
      pages/starter/
      目录名称(需与
      routes.yaml
      中的键匹配)
    • 在所有文件中批量替换
      nb_starter_projects
  2. 调整字段列表:在你的集合YAML文件中匹配用户的实体结构。同时更新
    pages/<...>/layout.yaml
    中的
    fields:
    以及弹窗模板的
    field_layout:
  3. 添加第二个实体:仅当第一个实体端到端运行正常后再添加。创建新的
    collections/*.yaml
    文件+
    pages/<module>/<entity>/
    目录,复制
    pages/starter/projects/layout.yaml
    作为起点。
  4. 增量扩展:添加标签页、图表、工作流触发器。每次变更后推送。请见下文“高级模式”,查看哪种CRM文件匹配对应的模式。
切勿一次性编写完整模块。面向客户的构建流程:先搭建骨架,展示给用户,收集反馈,再迭代。推送启动模板只需几分钟,而手动构建模块需要数小时。

Fast-track: when
--copy
helps

快速技巧:何时使用
--copy
参数

Pass
--copy
when the workspace has no popup files yet (early stage — validator would fire errors about "m2o field X has no popup binding" that the user will fix in the next push). The reconciler auto-bypasses spec errors in this state. Once any
popups/*.yaml
exists, drop
--copy
and let validation run.
当工作区还没有弹窗文件时(早期阶段——验证器会触发“m2o字段X无弹窗绑定”的错误,用户会在下一次推送时修复),使用
--copy
参数。协调器会自动绕过此状态下的规范错误。一旦存在任何
popups/*.yaml
文件,就不再使用
--copy
参数,让验证器正常运行。

Incremental edits — existing workspace

增量编辑——现有工作区

  • Add a block / field / action / popup → write the DSL → push.
  • Remove from DSL → push. The reconciler destroys the matching live model on the NB side and cleans
    state.yaml
    . Manual NB-UI authored elements (not tracked in
    state.yaml
    ) are left alone.
  • Rename → not supported automatically. Delete + re-add.
Targeted pushes:
  • --group <key>
    scopes to one menu subtree
  • --page <key>
    scopes to one page
  • --incremental
    skips pages whose DSL hasn't changed since last push
For pure live-UI tweaks without a DSL commit, hand off to
nocobase-ui-builder
instead
(see the routing table above).
  • 添加区块/字段/操作/弹窗 → 编写DSL → 推送。
  • 删除DSL中的内容 → 推送。协调器会在NocoBase端销毁对应的实时模型并清理
    state.yaml
    。手动通过NB-UI创作的元素(未在
    state.yaml
    中跟踪)会保留。
  • 重命名 → 不支持自动重命名。请先删除再重新添加。
定向推送:
  • --group <key>
    :仅作用于一个菜单子树
  • --page <key>
    :仅作用于一个页面
  • --incremental
    :跳过自上次推送以来DSL未变更的页面
对于无需提交DSL的纯实时UI调整,请转交至
nocobase-ui-builder
(见上方路由表)。

Advanced workflow — when the starter isn't enough

高级工作流——当启动模板不足以满足需求时

Triggers for going beyond the agile starter loop:
  • More than ~3 collections with cross-relations (m2m, tree structures)
  • Dedicated workflow / approval / permission logic that the user wants designed up-front
  • Multi-tab pages, sub-tables, or cross-module navigation
  • Dashboard with bespoke KPIs mapped to the user's domain language
Progression: Round 0 design → Round 1 scaffold → Round 2 fill → Round 2' seed → Round 3 JS. Each round is a deployable state.
需要超越敏捷启动模板流程的场景:
  • 超过约3个带有交叉关联(m2m、树形结构)的集合
  • 用户需要预先设计的专用工作流/审批/权限逻辑
  • 多标签页页面、子表格或跨模块导航
  • 包含与用户业务领域语言匹配的定制KPI的仪表盘
流程:第0轮设计 → 第1轮搭建骨架 → 第2轮填充内容 → 第2'轮测试数据 → 第3轮JS开发。每一轮都是可部署的状态。

Round 0: System architecture — confirm with user

第0轮:系统架构——与用户确认

Write a
DESIGN.md
(markdown, not YAML) covering:
  1. Collections — every table, its fields, and its relations. See
    nocobase-data-modeling
    skill for field-interface reference.
  2. Page list — every page, one-line purpose each, grouped by menu.
  3. Navigation wiring — which m2o fields open which popup templates; which pages link to each other.
Wait for user confirmation before writing YAML. A single design pass saves 3× redesigns.
Skip Round 0 if the user's ask fits the starter shape (single entity, basic CRUD).
Example:
markdown
undefined
编写
DESIGN.md
(Markdown格式,非YAML),涵盖以下内容:
  1. 集合——每个表、其字段及关联关系。请参考
    nocobase-data-modeling
    技能的字段接口说明。
  2. 页面列表——每个页面,一行描述其用途,按菜单分组。
  3. 导航配置——哪些m2o字段打开哪些弹窗模板;哪些页面相互链接。
在编写YAML前等待用户确认。一次设计可以避免3次重新设计。
如果用户的需求符合启动模板的结构(单个实体、基础CRUD),可跳过第0轮。
示例:
markdown
undefined

Collections

集合

  • nb_lib_books (title, author, isbn, category, status, loans: o2m → nb_lib_loans)
  • nb_lib_members (name, email, phone, join_date, loans: o2m → nb_lib_loans)
  • nb_lib_loans (loan_no, book: m2o, member: m2o, borrowed_at, due_date, returned_at, status)
  • nb_lib_books(title, author, isbn, category, status, loans: o2m → nb_lib_loans)
  • nb_lib_members(name, email, phone, join_date, loans: o2m → nb_lib_loans)
  • nb_lib_loans(loan_no, book: m2o, member: m2o, borrowed_at, due_date, returned_at, status)

Pages (under menu "Library")

页面(菜单“Library”下)

  • Books list — browse + search books, add new
  • Members list — browse members, their loan history
  • Loans list — active/overdue loans, return action
  • Dashboard — KPIs + charts
  • 书籍列表——浏览+搜索书籍,添加新书
  • 会员列表——浏览会员及其借阅历史
  • 借阅列表——活跃/逾期借阅,归还操作
  • 仪表盘——KPIs+图表

Navigation

导航

  • books.table.title → books detail popup (shared template)
  • loans.table.book → books detail popup (shared via defaults.yaml)
  • loans.table.member → members detail popup (shared via defaults.yaml)
undefined
  • books.table.title → 书籍详情弹窗(共享模板)
  • loans.table.book → 书籍详情弹窗(通过defaults.yaml共享)
  • loans.table.member → 会员详情弹窗(通过defaults.yaml共享)
undefined

Round 0.5: Sub-agent CWD (only when spawning)

第0.5轮:子代理工作目录(仅当生成子代理时)

If launching a sub-agent (kimi TUI, Claude Code subprocess), its CWD becomes the default write target. Set it before launch:
bash
mkdir -p <user-workdir>
cd <user-workdir>
kimi --yolo       # or claude, codex
Skip the
cd
and the agent writes to the parent project root.
如果启动子代理(kimi TUI、Claude Code子进程),其工作目录会成为默认写入目标。在启动前设置
bash
mkdir -p <user-workdir>
cd <user-workdir>
kimi --yolo       # 或claude、codex
如果跳过
cd
命令,代理会写入父项目根目录。

Round 1: Scaffold — still start from the starter

第1轮:搭建骨架——仍从启动模板开始

Even in the advanced path, don't hand-write
routes.yaml
+
collections/*.yaml
from scratch. Copy the starter, then grow:
StepWhat to do
Base
cp -r templates/starter workspaces/<name>
— push once
Add collectionWrite
collections/<next_coll>.yaml
(format matches starter's). Match
nb_<module>_<entity>
convention.
Add page
mkdir pages/<module>/<page>/
+
layout.yaml
; mirror starter's projects layout.
Update routesAdd entry under the existing group in
routes.yaml
.
Deploy
cli push <name> --force
after each collection/page addition
CRM references (consult only when stuck on structure):
  • templates/crm/collections/nb_crm_leads.yaml
    — collection format
  • templates/crm/routes.yaml
    — multi-group routes (shape only)
即使在高级流程中,也不要从头手动编写
routes.yaml
+
collections/*.yaml
。复制启动模板,再逐步扩展:
步骤操作
基础
cp -r templates/starter workspaces/<name>
——推送一次
添加集合编写
collections/<next_coll>.yaml
(格式与启动模板一致)。遵循
nb_<module>_<entity>
命名规范。
添加页面创建
pages/<module>/<page>/
目录+
layout.yaml
;镜像启动模板的projects页面布局。
更新路由
routes.yaml
的现有组下添加条目。
部署每次添加集合/页面后执行
cli push <name> --force
CRM参考(仅当结构上遇到问题时查阅):
  • templates/crm/collections/nb_crm_leads.yaml
    ——集合格式
  • templates/crm/routes.yaml
    ——多组路由(仅参考结构)

Round 2: Fill content — blocks, popups, templates

第2轮:填充内容——区块、弹窗、模板

For each page beyond the starter basics:
BuildingCRM reference
Main list table + filter
templates/crm/pages/main/leads/layout.yaml
Multi-tab page
templates/crm/pages/main/customers/
(
page.yaml
+
tab_*/layout.yaml
)
Create-form with inline sub-table for o2m children
templates/crm/templates/block/form_add_new_opportunities_quotations_quotations.yaml
items
is an o2m field listed in
fields:
and rendered as an inline editable sub-table. Also
templates/crm/pages/main/products/
for master/child UX.
Detail-popup template
templates/crm/templates/popup/activity_view.yaml
m2o auto-popup bindings
templates/crm/defaults.yaml
Parent-detail + child-list popup
templates/crm/pages/main/customers/tab_customers/popups/
addNew + field click-popup pattern
templates/crm/pages/main/leads/popups/
Rules for copying from CRM:
  • Copy 10–30 lines, adapt names. Never copy whole files.
  • Don't copy
    uid:
    /
    targetUid:
    /
    route_id:
    — deployer assigns fresh.
  • For every m2o field displayed in a table, either set
    clickToOpen: templates/popup/popup_detail_<target>.yaml
    OR add
    popups.<target>: ...
    in
    defaults.yaml
    . Validator errors otherwise.
对于超出启动模板基础功能的每个页面:
构建内容CRM参考
主列表表格+筛选
templates/crm/pages/main/leads/layout.yaml
多标签页页面
templates/crm/pages/main/customers/
page.yaml
+
tab_*/layout.yaml
包含o2m子项内联子表格的创建表单
templates/crm/templates/block/form_add_new_opportunities_quotations_quotations.yaml
——
items
fields:
中列出的o2m字段,渲染为可内联编辑的子表格。也可参考
templates/crm/pages/main/products/
的主/子UX。
详情弹窗模板
templates/crm/templates/popup/activity_view.yaml
m2o自动弹窗绑定
templates/crm/defaults.yaml
父详情+子列表弹窗
templates/crm/pages/main/customers/tab_customers/popups/
新建+字段点击弹窗模式
templates/crm/pages/main/leads/popups/
从CRM复制的规则:
  • 复制10-30行代码,适配名称。切勿复制整个文件
  • 不要复制
    uid:
    /
    targetUid:
    /
    route_id:
    ——部署器会自动分配新的ID。
  • 对于表格中显示的每个m2o字段,要么设置
    clickToOpen: templates/popup/popup_detail_<target>.yaml
    ,要么在
    defaults.yaml
    中添加
    popups.<target>: ...
    。否则会触发验证器错误。

Per-row actions (
recordActions
)

每行操作(
recordActions

By default a table's row-action column is empty — NB won't render any action buttons unless
recordActions:
lists them. "Just edit and delete" is a common but weak default: for most list tables the user actually wants a one-click state change (Mark Done, Approve, Archive) sitting next to edit.
Decision order:
  1. Is there a boolean / enum status field? → add
    updateRecord
    with
    linkageRules
    to show the matching button only when the record is in the right state. One button per state transition (Mark Done hidden when already done; Reopen hidden when not done).
  2. Does the record need a second detail/form view different from the default edit popup?
    popup
    with
    templateRef
    .
  3. Tree/hierarchy collection? → add
    addChild
    .
  4. Need to navigate elsewhere with this record's id/filter?
    link
    with
    url: /admin/...?filter={{ctx.record.id}}
    .
  5. Want to let the user clone a complex record?
    duplicate
    .
  6. Need to start a workflow manually?
    workflowTrigger
    .
Prefer
updateRecord + linkageRules
over custom JS buttons
for state changes. linkageRules covers 80% of row-level UX (conditional show/hide, field-based gating, role checks via
ctx.user
) without touching JS. JS actions are only needed for multi-step logic (query, then update, then navigate) and are not currently deployable by this reconciler.
Full per-row action palette (declared in
recordActions:
):
DSL
type
PurposeCRM reference
edit
Edit popup (default shape)many
view
Read-only detail popup
templates/crm/templates/popup/opportunity_view.yaml
delete
Single-row delete with confirmmany
updateRecord
Assign fields + optional linkageRules — the state-change workhorse
templates/crm/pages/main/overview/layout.yaml
(Done / Undone pair)
popup
Open a custom popup with
templateRef
(form/detail different from edit)
templates/crm/pages/main/leads/popups/table.name.yaml
link
Navigate to another admin page, carrying record context in URL
templates/crm/pages/lookup/layout.yaml
addChild
Tree collection: add a child node under this row
templates/crm/pages/main/products/tab_categories/layout.yaml
duplicate
Clone the record into a new form
workflowTrigger
Manually trigger a workflow on this record(toolbar in
templates/crm/pages/main/customers/tab_customers/layout.yaml
— same shape works per-row)
historyExpand
/
historyCollapse
Record-history plugin inline expand
ai
AI employee button (tasks_file + employee)
templates/crm/pages/main/leads/layout.yaml
(toolbar — same shape per-row)
Toolbar actions (declared in block-level
actions:
) use the same type names plus
filter
,
refresh
,
addNew
,
bulkDelete
,
export
,
import
. Not every type makes sense in both contexts —
updateRecord
as a toolbar action would apply to no specific row, so put it in
recordActions
.
Goal of Round 2: all pages have working CRUD — add / edit / view popups wired correctly, row-action columns reflect real per-record operations (not just edit+delete). Validator clean, NB UI shows no "Collection may have been deleted" banners.
默认情况下,表格的行操作列是空的——除非
recordActions:
列出操作,否则NB不会渲染任何操作按钮。“仅编辑和删除”是常见但较弱的默认设置:对于大多数列表表格,用户实际上需要一键状态变更(标记完成、批准、归档)按钮,与编辑按钮并列。
决策顺序:
  1. 是否存在布尔/枚举状态字段? → 添加
    updateRecord
    ,并使用
    linkageRules
    仅当记录处于正确状态时显示匹配按钮。每个状态转换对应一个按钮(记录已完成时隐藏“标记完成”按钮;记录未完成时隐藏“重新打开”按钮)。
  2. 记录是否需要与默认编辑弹窗不同的第二个详情/表单视图? → 使用
    popup
    并设置
    templateRef
  3. 是否为树形/层级集合? → 添加
    addChild
  4. 是否需要携带此记录的ID/筛选条件导航到其他页面? → 使用
    link
    并设置
    url: /admin/...?filter={{ctx.record.id}}
  5. 是否允许用户克隆复杂记录? → 添加
    duplicate
  6. 是否需要手动启动工作流? → 添加
    workflowTrigger
对于状态变更,优先使用
updateRecord + linkageRules
而非自定义JS按钮
。linkageRules覆盖了80%的行级UX(条件显示/隐藏、基于字段的限制、通过
ctx.user
进行角色检查),无需接触JS。仅当需要多步骤逻辑(查询、更新、导航)时才需要JS操作,且目前该协调器不支持部署JS操作。
完整的每行操作选项(在
recordActions:
中声明):
DSL
type
用途CRM参考
edit
编辑弹窗(默认结构)多处
view
只读详情弹窗
templates/crm/templates/popup/opportunity_view.yaml
delete
单行删除(带确认)多处
updateRecord
分配字段+可选linkageRules——状态变更的核心操作
templates/crm/pages/main/overview/layout.yaml
(完成/未完成按钮组)
popup
使用
templateRef
打开自定义弹窗(与编辑弹窗不同的表单/详情)
templates/crm/pages/main/leads/popups/table.name.yaml
link
导航到另一个管理页面,在URL中携带记录上下文
templates/crm/pages/lookup/layout.yaml
addChild
树形集合:在此行下添加子节点
templates/crm/pages/main/products/tab_categories/layout.yaml
duplicate
克隆记录到新表单
workflowTrigger
手动触发针对此记录的工作流
templates/crm/pages/main/customers/tab_customers/layout.yaml
中的工具栏——相同结构适用于每行)
historyExpand
/
historyCollapse
记录历史插件内联展开
ai
AI员工按钮(tasks_file + employee)
templates/crm/pages/main/leads/layout.yaml
(工具栏——相同结构适用于每行)
工具栏操作(在区块级
actions:
中声明)使用相同的类型名称,外加
filter
refresh
addNew
bulkDelete
export
import
。并非所有类型都适用于两种场景——
updateRecord
作为工具栏操作不会应用于特定行,因此应放在
recordActions
中。
第2轮目标:所有页面具备可用的CRUD功能——添加/编辑/查看弹窗配置正确,行操作列反映真实的每条记录操作(而非仅编辑+删除)。验证器无错误,NB UI无“集合可能已被删除”的提示。

Round 2': Test data (parallel with Round 2)

第2'轮:测试数据(与第2轮并行)

Can run concurrently with page-filling. Insert data via API:
bash
TOKEN=$(curl -sS -X POST $NB_URL/api/auth:signIn \
  -H 'Content-Type: application/json' -H 'X-Authenticator: basic' \
  -d '{"account":"'$NB_USER'","password":"'$NB_PASSWORD'"}' \
  | python3 -c 'import json,sys;print(json.load(sys.stdin)["data"]["token"])')
可与页面填充同时进行。通过API插入数据:
bash
TOKEN=$(curl -sS -X POST $NB_URL/api/auth:signIn \
  -H 'Content-Type: application/json' -H 'X-Authenticator: basic' \
  -d '{"account":"'$NB_USER'","password":"'$NB_PASSWORD'"}' \
  | python3 -c 'import json,sys;print(json.load(sys.stdin)["data"]["token"])')

Always GET existing record IDs first — they're snowflake integers

始终先获取现有记录ID——它们是雪花整数

(e.g. 359571523764224), NEVER 1/2/3.

curl -sS -X POST $NB_URL/api/<collection>:create -H "Authorization: Bearer $TOKEN"
-H 'Content-Type: application/json' -d '{...fields..., "owner":{"id": <real-user-id>}}'

Parent tables first; fill every FK on children (leaving it null
orphans the row). Then:

```bash
npx tsx cli/cli.ts verify-data <name>     # FK & completeness check
Why parallel: Round 3 JS (charts, KPIs) needs data to render anything. Start the seed once you have collections (end of Round 1); by the time Round 2 finishes pages, you have records to test against.
#(例如359571523764224),绝不是1/2/3。 curl -sS -X POST $NB_URL/api/<collection>:create -H "Authorization: Bearer $TOKEN"
-H 'Content-Type: application/json' -d '{...fields..., "owner":{"id": <real-user-id>}}'

先填充父表;填充子表的每个外键(留空会导致行成为孤儿记录)。然后执行:

```bash
npx tsx cli/cli.ts verify-data <name>     # 外键及完整性检查
为什么并行:第3轮JS开发(图表、KPIs)需要数据才能渲染内容。在完成第1轮(集合创建)后开始插入测试数据;当第2轮页面填充完成时,你已有可测试的记录。

Round 3: JS — where CRM uses it, you probably need it

第3轮:JS开发——CRM使用JS的场景,你可能也需要

Now that CRUD + data work, audit where JavaScript adds value. Walk the CRM template and ask "does the CRM have JS here?" for each spot in your project. Three typical JS opportunities:
SpotCRM has JS?Your project likely needs JS if...
Field renderer / column (e.g. color-coded status tag, days-until-due badge)✅ in most list tablesAny field whose display depends on a derived value (date math, status-to-color, multi-field composite)
Block (KPI card, custom widget inside a form)✅ overview, analytics, per-form tipsA summary widget, inline chart, or "helper panel" that reads from multiple collections
Dashboard page (whole page of charts + KPIs)✅ analytics pageModule has ≥3 measurable metrics users care about; validator requires ≥5 charts on pages titled "Dashboard" / "Analytics"
Start by grepping the matching CRM page and its
js/
+
charts/
folders to confirm where the CRM adds JS. Then write YOUR JS file adapted from the single-file table below.
Dashboards specifically look bad when designed freehand — mirror the CRM shape. Open the reference layout first, copy its block count, ordering, and grid widths into your own
layout.yaml
; then fill in leaf files with your content.
Reference layoutWhat to mirror
templates/crm/pages/main/overview/layout.yaml
Overview: 1 jsBlock hero row, 2 small tables underneath. Use for a landing page with a few KPIs.
templates/crm/pages/main/analytics/layout.yaml
Full dashboard: filterForm row → 4 KPI jsBlocks in one row → 5 charts in a
16/8 ∣ full ∣ 14/10
grid. Use when you want ≥5 charts (validator requires this when the page title contains
dashboard
or
analytics
).
Procedure:
  1. Open the reference
    layout.yaml
    . Note the block keys / types / widths.
  2. Write YOUR
    layout.yaml
    with the SAME shape — same number of blocks, same grid widths in the
    layout:
    section — but your own block keys and your own collection names.
  3. For each block's leaf JS/SQL file, copy from the single-file table below. Copy files individually; do not
    cp -r
    the folder.
Leaf file to copyReference
KPI card jsBlock
templates/crm/pages/main/overview/js/overview_jsBlock.js
Filtered summary jsBlock
templates/crm/pages/main/analytics/js/analytics_jsBlock.js
Chart SQL (grouped counts)
templates/crm/pages/main/analytics/charts/analytics_chart_2.sql
Chart render (echarts bar/pie)
templates/crm/pages/main/analytics/charts/analytics_chart_2_render.js
Filter stat buttons on filterForm
templates/crm/pages/main/customers/tab_customers/js/customers_customers_filterForm_customer_stats_filter_block.js
Full-page custom UI (wizard / multi-step / custom flow)
templates/crm/pages/main/customers/tab_merge/js/customers_merge_jsBlock.js
— whole page is one
type: jsBlock
, ~580 lines React
After copying each leaf file:
  • Rename in place and retarget SQL/collection/field names.
  • Remove
    ns: 'nb_crm'
    i18n wrappers unless your module has i18n.
  • Simplify
    ctx.var_form1.*
    filter var references if your page's filterForm uses different field keys.
SQL charts: save + run as a two-step pattern —
ctx.sql.save({uid, sql})
then
ctx.sql.runById(uid)
.
在CRUD和数据都正常工作后,评估JavaScript能带来的价值。遍历CRM模板,针对项目中的每个位置问自己“CRM在此处使用了JS吗?”。三种典型的JS应用场景:
场景CRM使用了JS?你的项目可能需要JS的情况
字段渲染器/列(例如颜色编码的状态标签、到期天数徽章)✅ 大多数列表表格任何显示依赖于派生值(日期计算、状态转颜色、多字段组合)的字段
区块(KPI卡片、表单内的自定义小部件)✅ 概览、分析、表单提示汇总小部件、内联图表或读取多个集合的“辅助面板”
仪表盘页面(全页图表+KPIs)✅ 分析页面模块包含≥3个用户关心的可衡量指标;验证器要求标题包含“Dashboard”/“Analytics”的页面至少有5个图表
首先通过grep查找匹配的CRM页面及其
js/
+
charts/
文件夹,确认CRM在哪里添加了JS。然后根据下表的单文件示例编写你自己的JS文件。
仪表盘自由设计效果很差——请镜像CRM的结构。先打开参考布局,将其区块数量、顺序和网格宽度复制到你自己的
layout.yaml
中;然后用你的内容填充叶子文件。
参考布局需镜像的内容
templates/crm/pages/main/overview/layout.yaml
概览:1个jsBlock hero行,下方2个小表格。用于包含少量KPIs的着陆页。
templates/crm/pages/main/analytics/layout.yaml
完整仪表盘:筛选表单行 → 一行4个KPI jsBlocks → 5个图表按
16/8 ∣ full ∣ 14/10
网格排列。当你需要≥5个图表时使用(页面标题包含
dashboard
analytics
时,验证器要求如此)。
步骤:
  1. 打开参考
    layout.yaml
    。记录区块键/类型/宽度。
  2. 编写你自己的
    layout.yaml
    ,保持相同结构——相同数量的区块,
    layout:
    部分相同的网格宽度,但使用你自己的区块键和集合名称。
  3. 对于每个区块的叶子JS/SQL文件,复制下表中的示例。单独复制文件;不要
    cp -r
    整个文件夹。
需复制的叶子文件参考
KPI卡片jsBlock
templates/crm/pages/main/overview/js/overview_jsBlock.js
筛选汇总jsBlock
templates/crm/pages/main/analytics/js/analytics_jsBlock.js
图表SQL(分组计数)
templates/crm/pages/main/analytics/charts/analytics_chart_2.sql
图表渲染(echarts柱状图/饼图)
templates/crm/pages/main/analytics/charts/analytics_chart_2_render.js
筛选表单上的统计按钮
templates/crm/pages/main/customers/tab_customers/js/customers_customers_filterForm_customer_stats_filter_block.js
全页自定义UI(向导/多步骤/自定义流程)
templates/crm/pages/main/customers/tab_merge/js/customers_merge_jsBlock.js
——整页是一个
type: jsBlock
,约580行React代码
复制每个叶子文件后:
  • 重命名并调整SQL/集合/字段名称。
  • 移除
    ns: 'nb_crm'
    国际化包装,除非你的模块支持国际化。
  • 如果你的页面筛选表单使用不同的字段键,请简化
    ctx.var_form1.*
    筛选变量引用。
SQL图表:采用两步模式保存并运行——
ctx.sql.save({uid, sql})
然后
ctx.sql.runById(uid)

Core concepts

核心概念

Two identifiers:
key
and
title

两种标识符:
key
title

key
= lower_snake_ascii identity — drives directory names under
pages/
and entries in
state.yaml
. Always write it explicitly when the title isn't pure ASCII (Chinese/spaces slugify to gibberish).
title
= display text as the user wants it shown.
yaml
- key: it_ops
  title: IT 运维
  type: group
  children:
    - key: tickets
      title: 工单
key
= 小写蛇形ASCII标识——决定
pages/
下的目录名称和
state.yaml
中的条目。当标题不是纯ASCII(中文/空格会被转换为无意义字符)时,务必显式编写
key
title
= 用户希望显示的文本。
yaml
- key: it_ops
  title: IT 运维
  type: group
  children:
    - key: tickets
      title: 工单

Two popup modes:
key: reference
vs bare
ref:

两种弹窗模式:
key: reference
vs 裸
ref:

key: reference
— popup block is a reference to the template. Editing the template updates every popup that references it. Use for any shared Add/Edit form.
yaml
blocks:
  - ref: templates/block/form_add_new_tickets.yaml
    key: reference           # REQUIRED for shared refs
Bare
ref:
(no
key: reference
) — template content is inlined per popup; each copy is independent. Use only to factor a bulky block out of the page file.
After deploy, a shared template's
usageCount
should be ≥ 1. If it stays at 0,
key: reference
was forgotten.
key: reference
——弹窗区块是对模板的引用。编辑模板会更新所有引用该模板的弹窗。适用于任何共享的添加/编辑表单。
yaml
blocks:
  - ref: templates/block/form_add_new_tickets.yaml
    key: reference           # 共享引用必填
ref:
(无
key: reference
)——模板内容会内联到每个弹窗中;每个副本都是独立的。仅用于将庞大的区块从页面文件中拆分出来。
部署后,共享模板的
usageCount
应≥1。如果始终为0,说明遗漏了
key: reference

Auto-created columns — do NOT declare them

自动创建的列——请勿声明

NocoBase auto-creates these; declaring them causes silent filtering or type conflicts:
  • System columns:
    id
    ,
    createdAt
    ,
    updatedAt
    ,
    createdBy
    ,
    updatedBy
  • m2o / o2m FK columns: declaring
    owner: m2o → users
    auto-creates
    owner_id
    ; don't add a second
    owner_id: integer
    row
  • m2m join tables:
    through: nb_x_y
    is auto-created; don't write a collection YAML for it
NocoBase会自动创建以下列;声明这些列会导致静默过滤或类型冲突:
  • 系统列:
    id
    createdAt
    updatedAt
    createdBy
    updatedBy
  • m2o/o2m外键列:声明
    owner: m2o → users
    会自动创建
    owner_id
    ;不要额外添加
    owner_id: integer
  • m2m关联表:
    through: nb_x_y
    会自动创建;不要为其编写集合YAML文件

Table vs sub-table — two different things, don't confuse

表格 vs 子表格——两种不同的事物,请勿混淆

TableSub-table
What it isFull CRUD block (filter + list + add/edit popups) for child records of a parentInline editable grid for child rows, lives INSIDE a parent form
DSL
type: table
+
resource_binding.sourceId + associationName
{ field: tasks, type: subTable, columns: [...] }
inside a createForm/editForm's
fields:
Where usedDetail popup, tab page, standalone-list popupInside createForm / editForm
Use whenChildren browsed separately (customer detail → orders list)Children entered alongside parent (invoice + line items)
Bare
- tasks
in a form is the third option: a RecordSelect picker ("pick existing record"). Rarely what you want — validator warns.
Canonical CRM examples:
  • Table (standalone CRUD block):
    templates/crm/pages/main/customers/tab_customers/popups/table.name.yaml
  • Sub-table (inline editable grid):
    templates/crm/templates/block/form_add_new_opportunities_quotations_quotations.yaml
    (
    items
    )
表格子表格
定义用于父记录子项的完整CRUD区块(筛选+列表+添加/编辑弹窗)用于子行的内联可编辑网格,位于父表单内部
DSL配置
type: table
+
resource_binding.sourceId + associationName
在createForm/editForm的
fields:
中配置
{ field: tasks, type: subTable, columns: [...] }
使用场景详情弹窗、标签页页面、独立列表弹窗createForm/editForm内部
使用时机子项可单独浏览(客户详情→订单列表)子项与父项一同录入(发票+行项目)
表单中仅写
- tasks
是第三种选项:RecordSelect选择器(“选择现有记录”)。很少是你想要的——验证器会发出警告。
CRM典型示例:
  • 表格(独立CRUD区块):
    templates/crm/pages/main/customers/tab_customers/popups/table.name.yaml
  • 子表格(内联可编辑网格):
    templates/crm/templates/block/form_add_new_opportunities_quotations_quotations.yaml
    items
    字段)

foreignKey
flips meaning

foreignKey
含义反转

On m2o,
foreignKey
names the FK column on the current table (
owner: m2o, foreignKey: owner_id
owner_id
on SELF).
On o2m,
foreignKey
names the FK column on the target table (
tasks: o2m → nb_pm_tasks, foreignKey: project_id
project_id
on
nb_pm_tasks
).
m2o中,
foreignKey
当前表上的外键列(
owner: m2o, foreignKey: owner_id
owner_id
在自身表上)。
o2m中,
foreignKey
目标表上的外键列(
tasks: o2m → nb_pm_tasks, foreignKey: project_id
project_id
nb_pm_tasks
表上)。

Command reference

命令参考

bash
cd <skill-dir>/src
export NB_USER=... NB_PASSWORD=... NB_URL=...

npx tsx cli/cli.ts push <name> --force          # deploy DSL → NocoBase
npx tsx cli/cli.ts push <name> --group <key>    # only one subtree
npx tsx cli/cli.ts push <name> --incremental    # skip unchanged (git diff)

npx tsx cli/cli.ts pull <name>                  # NocoBase → DSL (full round-trip)
npx tsx cli/cli.ts diff <left> <right>          # compare two DSL trees
npx tsx cli/cli.ts duplicate-project <src> <dst> --key-suffix _v2

npx tsx cli/cli.ts verify-data <name>           # FK / completeness check
push and pull are both one-way. Round-tripping = push + pull + git diff.
bash
cd <skill-dir>/src
export NB_USER=... NB_PASSWORD=... NB_URL=...

npx tsx cli/cli.ts push <name> --force          # 将DSL部署到NocoBase
npx tsx cli/cli.ts push <name> --group <key>    # 仅部署一个子树
npx tsx cli/cli.ts push <name> --incremental    # 跳过未变更的内容(基于git diff)

npx tsx cli/cli.ts pull <name>                  # 将NocoBase内容同步到DSL(完整往返)
npx tsx cli/cli.ts diff <left> <right>          # 比较两个DSL树
npx tsx cli/cli.ts duplicate-project <src> <dst> --key-suffix _v2

npx tsx cli/cli.ts verify-data <name>           # 外键/完整性检查
push和pull都是单向操作。往返同步=push+pull+git diff。

Common errors

常见错误

ErrorFix
fields not in collection
Field names don't match the collection YAML
titleField is missing
Set
titleField: <field>
or add a
name
/
title
field
Only some pages deployed
key
mismatch with a
pages/<key>/
directory
string violation
on create
createdAt
/
updatedAt
declared in YAML — remove
Chart SQL failedSeed data first; quote field names like
"createdAt"
m2o link 400 in UIMissing
defaults.yaml
popups:
binding for target collection
Collection X not found in data source main
associationName
used a short name — use the full collection name (
nb_pm_projects.tasks
, not
project.tasks
). See
templates/crm/pages/main/customers/tab_customers/popups/
Per-row column shows only edit+delete
recordActions
missing — see "Per-row actions" below for what to add. Removing an action/field/column from the DSL now does destroy it on the NB side on next push.

If any of the above contradicts what you observe at runtime, the manual is stale — note what was missing and tell the user.
错误修复方法
fields not in collection
字段名称与集合YAML不匹配
titleField is missing
设置
titleField: <field>
或添加
name
/
title
字段
仅部分页面部署成功
key
pages/<key>/
目录不匹配
创建时出现
string violation
YAML中声明了
createdAt
/
updatedAt
——移除这些声明
图表SQL执行失败先插入测试数据;字段名需加引号,如
"createdAt"
UI中m2o链接返回400错误目标集合缺少
defaults.yaml
中的
popups:
绑定
Collection X not found in data source main
associationName
使用了短名称——使用完整集合名称(
nb_pm_projects.tasks
,而非
project.tasks
)。请见
templates/crm/pages/main/customers/tab_customers/popups/
每行操作列仅显示编辑+删除缺少
recordActions
——请见下文“每行操作”部分添加对应的内容。现在从DSL中移除操作/字段/列会在下次推送时在NB端销毁对应的内容。

如果以上内容与你运行时观察到的情况不符,说明本手册已过时——请记录缺失的内容并告知用户。