mcpField
N
nocobase-dsl-reconciler

nocobase-dsl-reconciler

github.com/nocobase开发55242 次安装8 周前更新
代码仓库

关于 nocobase-dsl-reconciler

nocobase-dsl-reconciler 是用于通过已提交的 YAML DSL 文件构建 NocoBase 应用并通过 CLI 部署的可选技能。它引导从 starter 增量构建,在部署前验证规范,并区分 DSL 开发与实时 UI 修改。

主要功能

  1. 01从 starter 模板搭建应用
  2. 02用 YAML 定义集合、路由、页面和弹窗
  3. 03通过 CLI 推送变更到 NocoBase
  4. 04按组或页面定向增量部署
  5. 05部署前验证 DSL 规范

适用场景

  1. 01以已提交的 YAML 构建 CRUD 模块
  2. 02分阶段扩展应用中的集合和页面
  3. 03编写 YAML 前规划多集合结构
  4. 04将 DSL 变更部署到指定页面

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/nocobase/skills --skill nocobase-dsl-reconciler

安装后在 Claude Code 中输入/nocobase-dsl-reconciler确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

exports/crm-opportunities/
structure.yaml
exports/crm-opportunities/js/
opportunities_filterForm_0_pipeline_stats_filter_block.js
exports/crm-v2/
enhance.yaml
exports/crm-v2/charts/
analytics_chart_1.json
analytics_chart_2.json
analytics_chart_3.json
analytics_chart_4.json
analytics_chart_5.json
exports/crm-v2/js/
analytics_filterForm_0_js_0.js
analytics_jsBlock_6.js
analytics_jsBlock_7.js
analytics_jsBlock_8.js
analytics_jsBlock_9.js
customers_filterForm_1_customer_stats_filter_block.js
customers_popup_name_tab0_contacts_contact_row_column_customer_360.js
customers_popup_name_tab0_details_0_customer_statistics_block.js
customers_popup_name_tab0_details_0_item_profile_customer_profile_header.js
customers_popup_name_tab0_details_1_ai_smart_profile_block.js
customers_popup_name_tab0_opportunities_opportunity_row_column_customer_360.js
customers_table_0_col_health_score.js
customers_table_0_col_last_activity.js
leads_filterForm_1_leads_stats_filter_block.js
leads_popup_name_tab0_details_0_lead_badges_row_component.js
leads_popup_name_tab0_details_0_lead_key_stats_block.js
leads_popup_name_tab0_details_5_lead_stage_flow_block.js
leads_popup_name_tab0_lead_insights_lead_ai_insights_block.js
leads_popup_name_tab0_lead_owner_lead_profile_header_block.js
leads_table_0_col_ai_score.js
leads_table_0_col_contact.js
leads_table_0_col_created.js

详细介绍

NocoBase 应用构建器(DSL 路径)

使用此技能前

这是可选的 DSL 路径。默认使用 nocobase-ui-builder。 只有当用户明确需要可以提交的 YAML 文件,并希望执行 cli push 时,才继续使用此技能。如果你是从一般的“帮我构建一个 NocoBase 应用”请求进入此处,且用户没有提到 DSL/YAML/git,请改用 nocobase-ui-builder——这是默认入口。

黄金法则

templates/crm/ 是只读参考库。如果不确定某个字段、区块或弹窗的具体结构,请打开最接近的 CRM 示例,阅读后在自己的工作区中编写适配版本。下方的工作流章节中提供了各场景的参考指引。

切勿将 CRM 文件整体复制到工作区。不要使用 cp -r templates/crm/... 开始构建,不要复制 collections/nb_crm_*.yaml,也不要以 CRM 的文件为基础编写自己的 routes.yaml。批量复制会把无关的线索、商机、订单状态和工作流带入项目——之后整个开发过程都会耗费在处理数百条无关的验证错误上,而不是构建你的模块。

部署前的规范验证器可以发现大多数结构性错误,并给出清晰的错误消息。相信验证器:出现错误时,按提示修复,不要猜测——不要用 grep 搜索 src/deploy/*.ts。

不适用此技能的情况

如果用户的请求与此技能无关,请转交给对应技能:

用户请求……技能
对已运行页面进行一次性的实时 UI 调整(移动 / 重新排序 / 重新配置单个区块、字段、操作)——不需要提交 DSLnocobase-ui-builder
ACL / 角色权限 / 路由权限nocobase-acl-manage
创建 / 更新 / 修订 / 执行工作流nocobase-workflow-manage(此技能只负责接入触发按钮;工作流图的编写由该技能处理)
在 DSL 项目之外编写集合 / 字段 / 关联nocobase-data-modeling
插件开发(.tsx 组件、服务端代码)nocobase-plugin-development
安装 / 启用插件nocobase-plugin-manage
环境配置 / 应用安装 / 升级nocobase-env-manage

任何应该以已提交 YAML 文件的形式保存在 workspaces/<project>/ 下的变更——都属于此技能的范围。

环境

cd <skill-dir>/src
export [email protected] NB_PASSWORD=admin123 NB_URL=http://localhost:14000

快速开始——新建项目

“帮我构建一个 NocoBase 应用”的默认路径:复制起始模板并进行修改。不要手动编写骨架,也不要先研究 CRM。

cd <skill-dir>/src
cp -r ../templates/starter ../workspaces/<name>

# First push: --copy bypasses validation rules that only matter once
# popups exist (m2o popup binding, clickToOpen file presence). The
# 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

起始模板是完整的最小 CRUD,包含 1 个集合(Projects)、1 个仪表板页面(4 个 KPI 磁贴 + 2 个图表)、1 个列表页面(包含 filterForm / table / addNew 弹窗 / detail 弹窗 / 2 个 updateRecord recordActions)。按原样推送 → 在 NB 中查看 → 然后编辑。

自定义起始模板(敏捷迭代流程)

每次只迭代一个事项,并在每次迭代之间推送:

  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 文件。

**切勿一次性编写整个模块。**对于面向客户的构建,先提交骨架、展示给用户、收集反馈,再逐步迭代。推送起始模板只需几分钟;手动构建整个模块则需要数小时。

快速上手:何时适合使用 --copy

当工作区中还没有弹窗文件时,传入 --copy(早期阶段——验证器会报错,指出“m2o field X has no popup binding”,用户会在下一次推送时修复)。在这种状态下,协调器会自动绕过规范错误。一旦存在任何 popups/*.yaml 文件,就移除 --copy,让验证正常运行。

增量编辑——现有工作区

  • 添加区块/字段/操作/弹窗 → 编写 DSL → 推送。
  • 从 DSL 中移除 → 推送。协调器会销毁 NB 端对应的实时模型,并清理 state.yaml。手动通过 NB-UI 创建的元素(未在 state.yaml 中跟踪)会保留。
  • 重命名 → 不支持自动处理。先删除,再重新添加。

定向推送:

  • --group <key> 将范围限定为一个菜单子树
  • --page <key> 将范围限定为一个页面
  • --incremental 跳过自上次推送以来 DSL 未发生变化的页面

如果只是调整实时 UI,且没有提交 DSL,请改用 nocobase-ui-builder(参见上方的路由表)。

高级工作流——当入门模板不够用时

需要超出敏捷入门流程的情况:

  • 集合超过约 3 个,且存在交叉关系(m2m、树形结构)
  • 用户希望提前设计专用的工作流/审批/权限逻辑
  • 多标签页、子表或跨模块导航
  • 仪表板包含映射到用户领域语言的定制 KPI

流程:第 0 轮设计 → 第 1 轮搭建脚手架 → 第 2 轮填充 → 第 2' 轮填充种子数据 → 第 3 轮 JS。每一轮的状态都可部署。

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

编写 DESIGN.md(Markdown,而非 YAML),涵盖:

  1. 集合——每张表、其字段及其关系。 字段接口参考请参阅 nocobase-data-modeling 技能。
  2. 页面列表——每个页面及其一句话用途,按菜单分组。
  3. 导航连线——哪些 m2o 字段会打开哪些弹窗模板;哪些页面彼此链接。

编写 YAML 前,先等待用户确认。一次设计就能减少 3 倍的返工。

如果用户的需求符合入门模板的形态(单个实体、基础 CRUD),则跳过第 0 轮。

示例:

## 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)

## Pages (under menu "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

## 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)
第 0.5 轮:子 Agent 的 CWD(仅在启动子 Agent 时)

如果启动子 Agent(kimi TUI、Claude Code 子进程),其 CWD 会成为默认写入目标。启动前请设置好:

mkdir -p <user-workdir>
cd <user-workdir>
kimi --yolo       # or claude, codex

如果跳过 cd,Agent 就会写入父项目根目录。

第 1 轮:搭建脚手架——仍从入门模板开始

即使采用高级流程,也不要从头手写 routes.yaml + collections/*.yaml。复制入门模板,再逐步扩展:

步骤操作
基础cp -r templates/starter workspaces/<name>——推送一次
添加集合编写 collections/<next_coll>.yaml(格式与入门模板一致)。遵循 nb_<module>_<entity> 命名约定。
添加页面mkdir pages/<module>/<page>/ + layout.yaml;参照入门模板的 projects 布局。
更新路由在 routes.yaml 中现有分组下添加条目。
部署每次添加集合/页面后,执行 cli push <name> --force

CRM 参考(仅在结构方面遇到困难时查阅):

  • templates/crm/collections/nb_crm_leads.yaml——集合格式
  • templates/crm/routes.yaml——多分组路由(仅结构)
第 2 轮:填充内容——区块、弹出窗口、模板

对于入门基础之外的每个页面:

构建内容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/
addNew + 字段点击弹出窗口模式templates/crm/pages/main/leads/popups/

从 CRM 复制时的规则:

  • 复制 10–30 行,并调整名称。切勿复制整个文件。
  • 不要复制 uid: / targetUid: / route_id:——部署器会分配新的值。
  • 对于表格中显示的每个 m2o 字段,请设置 clickToOpen: templates/popup/popup_detail_<target>.yaml,或在 defaults.yaml 中添加 popups.<target>: ...。否则验证器会报错。
每行操作(recordActions)

默认情况下,表格的行操作列为空——除非 recordActions: 中列出了操作按钮,否则 NB 不会渲染任何操作按钮。“只提供编辑和删除”是常见但不够理想的默认设置:对于大多数列表表格,用户真正需要的是紧挨着编辑按钮的一键状态更改(标记为已完成、批准、归档)。

决策顺序:

  1. 是否有布尔值/枚举类型的状态字段? → 添加带有 linkageRules 的 updateRecord,仅在记录处于相应状态时显示匹配的按钮。每种状态转换对应一个按钮(已完成时隐藏“标记为已完成”;未完成时隐藏“重新打开”)。
  2. 记录是否需要与默认编辑弹窗不同的第二个详情/表单视图? → 使用带有 templateRef 的 popup。
  3. 是否为树形/层级集合? → 添加 addChild。
  4. 是否需要携带此记录的 id/筛选条件导航到其他位置? → 使用 link,并设置 url: /admin/...?filter={{ctx.record.id}}。
  5. 是否要让用户克隆复杂记录? → 使用 duplicate。
  6. 是否需要手动启动工作流? → 使用 workflowTrigger。

对于状态更改,优先使用 updateRecord + linkageRules,而不是自定义 JS 按钮。linkageRules 无需接触 JS,即可覆盖 80% 的行级 UX(条件显示/隐藏、基于字段的限制、通过 ctx.user 进行角色检查)。只有多步骤逻辑(查询,然后更新,再导航)才需要 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记录历史插件的行内展开—
aiAI 员工按钮(tasks_file + employee)templates/crm/pages/main/leads/layout.yaml(工具栏——相同样式也适用于每行)

工具栏操作(在区块级 actions: 中声明)使用相同的类型名称,另加 filter、refresh、addNew、bulkDelete、export、import。并非每种类型都适用于这两种上下文——作为工具栏操作的 updateRecord 不会针对任何特定行,因此应将其放在 recordActions 中。

第 2 轮目标:所有页面都具备可用的 CRUD——正确连接新增/编辑/查看弹窗,行操作列反映真实的逐条记录操作(而不只是编辑和删除)。验证器无报错,NB UI 不显示“集合可能已删除”横幅。

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

可与填充页面并行运行。通过 API 插入数据:

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
# (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>}}'

先处理父表;为子表填写所有外键(留空会导致该行成为孤儿)。然后:

npx tsx cli/cli.ts verify-data <name>     # FK & completeness check

为何并行:第 3 轮的 JS(图表、KPI)需要数据才能渲染内容。创建好集合后(第 1 轮结束时)就开始填充;到第 2 轮完成页面时,你就有记录可供测试。

第 3 轮:JS——CRM 用到的地方,你大概也需要

现在 CRUD + 数据已经能用了,接下来检查 JavaScript 能发挥作用的地方。浏览 CRM 模板,并针对项目中的每个位置询问“CRM 在这里有 JS 吗?” 三种典型的 JS 用途:

位置CRM 有 JS 吗?如果……,你的项目可能需要 JS
字段渲染器 / 列(例如,颜色编码的状态标签、距离到期日的天数徽标)✅ 大多数列表表格中都有字段显示依赖派生值(日期计算、状态转颜色、多字段组合)
区块(KPI 卡片、表单内的自定义小组件)✅ 概览、分析、各表单中的提示需要一个汇总小组件、内嵌图表,或从多个集合读取数据的“辅助面板”
仪表板页面(包含图表 + KPI 的整页)✅ 分析页面模块至少有 3 个用户关心的可衡量指标;对于标题为“Dashboard” / “Analytics”的页面,验证器要求至少有 5 个图表

先搜索对应的 CRM 页面及其 js/ 和 charts/ 文件夹,确认 CRM 在哪里添加了 JS。然后根据下方的单文件表编写适配你项目的 JS 文件。

仪表板如果凭空设计,通常效果很差——请参照 CRM 的结构。 先打开参考布局,将其区块数量、顺序和网格宽度复制到自己的 layout.yaml 中;然后填写各叶子文件的内容。

参考布局参照内容
templates/crm/pages/main/overview/layout.yaml概览:1 个 jsBlock 英雄区行,下方有 2 个小型表格。适用于包含少量 KPI 的着陆页。
templates/crm/pages/main/analytics/layout.yaml完整仪表板:筛选表单行 → 同一行 4 个 KPI jsBlock → 在 16/8 ∣ full ∣ 14/10 网格中排列 5 个图表。适用于需要至少 5 个图表的场景(当页面标题包含 dashboard 或 analytics 时,验证器要求至少有 5 个图表)。

步骤:

  1. 打开参考 layout.yaml。记下区块键 / 类型 / 宽度。
  2. 按相同结构编写自己的 layout.yaml——使用相同数量的区块,以及 layout: 部分中相同的网格宽度——但使用自己的区块键和集合名称。
  3. 对于每个区块的叶子 JS/SQL 文件,请从下方的单文件表中复制。逐个复制文件;不要使用 cp -r 复制整个文件夹。
要复制的叶子文件参考
KPI 卡片 jsBlocktemplates/crm/pages/main/overview/js/overview_jsBlock.js
筛选汇总 jsBlocktemplates/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
filterForm 上的筛选统计按钮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 / 集合 / 字段名称。
  • 如果你的模块没有 i18n,请移除 ns: 'nb_crm' i18n 包装。
  • 如果页面的 filterForm 使用了不同的字段键,请简化 ctx.var_form1.* 筛选变量引用。

SQL 图表:按两步模式保存并运行—— ctx.sql.save({uid, sql}) 然后 ctx.sql.runById(uid)。

核心概念

两种标识符:key 和 title

key = lower_snake_ascii 标识符——用于生成 pages/ 下的目录名称和 state.yaml 中的条目。如果标题不是纯 ASCII,请务必显式填写(中文 / 空格会被转换成乱码)。

title = 按用户意愿显示的文本。

- key: it_ops
  title: IT 运维
  type: group
  children:
    - key: tickets
      title: 工单
两种弹窗模式:key: reference 与裸 ref:

key: reference —— 弹窗区块是模板的引用。 编辑模板会更新所有引用它的弹窗。适用于任何共享的添加/编辑表单。

blocks:
  - ref: templates/block/form_add_new_tickets.yaml
    key: reference           # REQUIRED for shared refs

裸 ref:(没有 key: reference)—— 模板内容会按弹窗内联,每个副本彼此独立。仅用于将较大的区块从页面文件中提取出来。

部署后,共享模板的 usageCount 应 ≥ 1。如果仍为 0,说明漏掉了 key: reference。

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

NocoBase 会自动创建这些列;声明它们会导致静默过滤或类型冲突:

  • 系统列:id、createdAt、updatedAt、createdBy、updatedBy
  • m2o / o2m 外键列:声明 owner: m2o → users 会自动创建 owner_id;不要再添加第二个 owner_id: integer 行
  • m2m 联接表:through: nb_x_y 会自动创建;不要为它编写 collection YAML
Table 与 sub-table——两者不同,请勿混淆
TableSub-table
定义面向父记录子记录的完整 CRUD 区块(筛选器 + 列表 + 添加/编辑弹窗)用于子行的内联可编辑网格,位于父表单内部
DSLtype: table + resource_binding.sourceId + associationName位于 createForm/editForm 的 fields: 中的 { field: tasks, type: subTable, columns: [...] }
使用位置详情弹窗、标签页、独立列表弹窗createForm / editForm 内
适用场景单独浏览子记录(客户详情 → 订单列表)与父记录一起录入子记录(发票 + 行项目)

表单中裸写 - tasks 是第三种选项:RecordSelect 选择器(“选择现有记录”)。这通常不是你想要的——验证器会发出警告。

规范的 CRM 示例:

  • Table(独立 CRUD 区块):templates/crm/pages/main/customers/tab_customers/popups/table.name.yaml
  • Sub-table(内联可编辑网格):templates/crm/templates/block/form_add_new_opportunities_quotations_quotations.yaml(items)
foreignKey 会改变含义

在 m2o 中,foreignKey 指当前表上的外键列(owner: m2o, foreignKey: owner_id → SELF 上的 owner_id)。

在 o2m 中,foreignKey 指目标表上的外键列(tasks: o2m → nb_pm_tasks, foreignKey: project_id → nb_pm_tasks 上的 project_id)。

命令参考

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 和 pull 都是单向操作。往返操作 = push + pull + git diff。

常见错误

错误修复方法
fields not in collection字段名与 collection YAML 不匹配
titleField is missing设置 titleField: <field> 或添加 name/title 字段
仅部署了部分页面key 与 pages/<key>/ 目录不匹配
创建时出现 string violationYAML 中声明了 createdAt/updatedAt——请移除
图表 SQL 执行失败先填充种子数据;字段名需加引号,例如 "createdAt"
UI 中 m2o 链接返回 400缺少目标 collection 的 defaults.yaml popups: 绑定
Collection X not found in data source mainassociationName 使用了短名称——请使用完整 collection 名称(nb_pm_projects.tasks,而不是 project.tasks)。请参阅 templates/crm/pages/main/customers/tab_customers/popups/
每行列只显示编辑和删除缺少 recordActions——要添加的内容请参阅下文“每行操作”。现在,从 DSL 中移除操作/字段/列会在下次 push 时同步删除 NB 端对应内容。

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

评论

支持 Markdown