关于 add-sample-data
add-sample-data 是面向 Power Pages 代码应用的 Dataverse 数据填充技能,供已创建数据表的项目使用。它按已批准的表范围和依赖顺序生成情境化记录,并支持从原型种子数据复用。
主要功能
- 01按批准范围选择数据表
- 02按依赖顺序插入记录
- 03生成符合场景的样例数据
- 04支持原型种子数据复用
- 05跳过已达记录目标的表
- 06从部分完成状态安全重跑
适用场景
- 01为新建 Dataverse 表填充演示数据
- 02将原型种子数据映射到 Dataverse
- 03在部分插入失败后继续填充
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ microsoft/ power-platform-skills --skill add-sample-data安装后在 Claude Code 中输入/add-sample-data确认已加载。
详细介绍
插件检查:运行
node "${PLUGIN_ROOT}/scripts/check-version.js"——如果输出消息,请先向用户显示,然后再继续。
📋 共享说明:shared-instructions.md — 请先阅读。
添加示例数据
应用根目录: 在读取任何项目内容或执行任何命令之前,先执行
app-working-directory.md。
所有 shell 调用和文件工具(包括媒体路径和重试)都使用解析出的绝对路径 working_dir;
切勿沿用之前的 cd。
向 Dataverse 表填充真实感强的示例记录,让刚搭建完成的代码应用在首次启动时就能显示看起来真实的数据。根据每个表的架构生成行,并按依赖顺序插入。请在 /add-dataverse(或 /setup-datamodel)创建表后使用。
核心原则
- 覆盖已批准范围内的表,而非整个清单。 为每个符合条件的已批准表填充少量实用数据。清单是经过验证的应用库存,可能包含即将退役或不相关的表;某个表出现在清单中,并不代表可以为其填充数据。下文所有覆盖、扇出、原型复用、媒体和重试规则都不能凌驾于范围限制和退役排除规则之上。
- 插入顺序很重要。 必须先插入父表/被引用表,再插入子表/引用表,这样才能获取查找 ID。
- 使用符合上下文的数据,而非 Lorem Ipsum。 生成与列名和类型相符的值。在巡检应用中,
cr3e9_sitename列应填入“Westside Construction Site”,而不是“Sample Name 1”。 - 生成符合场景的行。 如果存在
native-app-plan.md,请阅读它,尤其是### Shared Conventions和 screen-templates.md 中定义的各屏幕Operational pattern值;否则,请使用当前数据请求和经过验证的架构。填充的行应能体现应用的实际工作流:状态、日期、关系、优先级/严重程度、媒体元数据,以及能让计划中的首屏内容丰富起来的边缘情况。 - 优雅处理失败。 插入失败时,记录错误并继续处理剩余记录——绝不自动回滚。用户修复问题后可以重新运行。
- 重新运行时保持幂等。 如果之前的运行只完成了部分操作,第二次运行时读取
memory-bank.md的已填充数据表,并跳过已插入的记录。 - 插入操作限定在解决方案内。 始终传入
--solution <uniqueName>,确保记录写入我们的解决方案,而非默认解决方案。
工作流
- 确定已批准的数据填充范围 → 1. 验证项目和身份验证 → 2. 发现范围内的表 → 3. 选择表和数量 → 4. 生成并预览 → 5. 插入 → 6. 汇总
步骤 0 — 确定已批准的种子范围
输入是技能参数,而不是 dataverse-request.js 的标志:
--working-dir <absolute-root>:继承调用方的根目录;拒绝冲突的显式/继承根目录。在嵌套调用中,绝不默认使用 shell 当前工作目录。--tables <comma-separated-logical-names>:确切的已批准种子表允许列表。每次编排调用都必须提供,包括全新创建。--exclude-tables <comma-separated-logical-names>:已批准编辑中要退役的表名;仅当没有表要退役时,才省略此参数或传入显式空字符串。
对于编排调用,在进行身份验证、记录数查询或生成之前,必须要求提供范围受限的交接信息以及匹配的 --tables。缺少允许列表或允许列表格式错误时,返回 NEEDS_CONTEXT;绝不回退到清单中的所有表。显式空允许列表表示不执行任何操作:报告没有已批准的种子表,并在不调用云服务或写入数据的情况下返回。
根据清单以及已批准计划/调用方范围验证逻辑名称。根据调用方的退役集合以及当前计划/历史记录中的任何待处理移除项构建 retiringTables;将这些表包含在 --exclude-tables 中。如果允许列表中的名称不存在/未经验证,或与 retiringTables 重叠,则在插入前停止并返回范围不匹配。不要静默丢弃未知名称,也不要从过渡中的清单条目推断许可。
对于独立请求,遵循显式指定的 --tables 和排除项。如果未提供表列表,则在步骤 2 中发现候选表,并在步骤 5 之前请用户批准具体的表集合/数量,然后将该选择固定为 seedTables。对于此独立发现路径,如果本地清单不存在,则根据已验证的元数据进行验证;绝不要将发现结果本身视为批准。待处理的退役项始终会排除候选表;如果状态不明确,应询问,而不是将清单视为批准。只读回退发现不代表获准向环境中填充种子数据。
在整个调用过程中保持固定的 seedTables 允许列表。之后所有选择、批次、媒体任务和恢复操作都必须限定在此范围内。不要修改 .datamodel-manifest.json 或应用计划来使被排除的表符合条件。要扩大范围,必须返回所属的审批关卡。
对于 --plan-only 或规划阶段交接,使用提供的拟议表范围进行只读检查,并在步骤 5 之前返回拟议的数量、依赖需求和媒体策略。规划批准不等于插入批准。此路径不得生成媒体文件、插入/更新/上传记录,也不得更改应用计划、清单或种子数据历史记录。
原型种子数据复用
--from-seed 由 /prototype-to-real-app 在将模拟原型转换为 Dataverse 后使用。在此模式下,优先复用现有原型种子文件,而不是生成新行:
src/generated/services/*/*.seed.json
src/generated/services/*.seed.json
仅使用 .datamodel-manifest.json 将 seedTables 对象映射到 Dataverse 负载;原型种子文件不得扩大已批准范围:
- 仅保留真实清单列的值。
- 将查找引用转换为清单中的精确
<schemaName>@odata.bind键。 - 保留清单中的选项集整数;不要根据标签臆造值。
- 跳过没有 Dataverse 列的仅限本地原型字段。
- 保持依赖层级插入顺序。
如果无法安全映射种子文件,则回退到为该表生成符合上下文的示例行,并在摘要中记录 DONE_WITH_CONCERNS。--from-seed 仅表示优先使用种子文件,并不代表获准插入格式错误的数据。
步骤 1 — 验证项目与身份验证
在每次 Shell 调用中,从解析出的 working_dir 运行应用本地命令。
cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
if [ ! -f power.config.json ] || [ ! -f app.config.js ]; then
echo "BLOCKED: working_dir is not an initialized app" >&2
exit 1
fi
environment_id="$(node -p "require('./power.config.json').environmentId || ''")" || exit 1
if [ -z "$environment_id" ]; then
printf '%s\n' 'ERROR: selected app has no environmentId' >&2
exit 1
fi
node "${PLUGIN_ROOT}/scripts/resolve-environment.js" "$environment_id" --no-cache --require-tenant
记录环境 URL、环境 ID和租户 ID,供后续脚本调用使用;这些信息必须与所选应用及任何提供的所有者上下文匹配。经过有限次数的只读恢复后,身份信息仍不完整或相互冲突时,返回 NEEDS_CONTEXT。不得移除安全标志、选择其他环境或保存解析器输出以进行恢复。
将后续每次 Dataverse 读取、批处理、PATCH、媒体上传、令牌刷新和重试都绑定到该确切的环境 URL 和租户 ID。对于每次 dataverse-request.js 调用,都要显式传入 --tenant-id '<tenantId-from-resolve-environment>';在每次新的 Shell 调用中重新使用经过验证的步骤 1 值。不得依赖之前的 Shell 导出、质询发现或当前 Azure CLI 租户。租户上下文缺失或冲突时,在调用前返回 NEEDS_CONTEXT。
验证 Azure CLI 身份验证(脚本需要 Azure CLI 令牌):
cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
az account show --query "user.name" -o tsv
如果为空,提示运行 az login 并停止。
步骤 2 — 发现表
遥测检查点:discover_dataverse_tables
步骤 2a — 路径 A:读取 .datamodel-manifest.json(首选)
cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
test -f .datamodel-manifest.json
如果存在,则解析 JSON。它包含经过验证的应用清单中的 logicalName、displayName、status(new / extended / reused)和 columns。
这是首选路径——速度快,且无需 API 调用。对于范围限定的调用,只能在执行任何逐表元数据或计数查询之前,使用经过验证的 seedTables 条目。对于独立发现,从候选列表中排除待退役表。
将无关的清单条目仅作为清单保留;不得将其传入记录计数、生成或插入循环。
跳过步骤 2b。
步骤 2b — 路径 B:查询 OData(回退方案)
如果在编排调用中缺少 .datamodel-manifest.json,则向所有者返回 BLOCKED,要求恢复经过验证的清单;不得扩大发现范围或臆造架构信息。对于带有明确允许列表的独立请求,只获取这些逻辑名称的元数据。以下广泛发现仅用于不带表列表的独立请求,以便提出候选项供审批:
cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
node "${PLUGIN_ROOT}/scripts/dataverse-request.js" <envUrl> GET \
"EntityDefinitions?\$select=LogicalName,DisplayName,EntitySetName&\$filter=IsCustomEntity eq true" \
--tenant-id '<tenantId-from-resolve-environment>'
对于项目使用的每个候选表(或每个明确限定范围的表),获取其自定义列:
cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
node "${PLUGIN_ROOT}/scripts/dataverse-request.js" <envUrl> GET \
"EntityDefinitions(LogicalName='<table>')/Attributes?\$select=LogicalName,DisplayName,AttributeType,RequiredLevel&\$filter=IsCustomAttribute eq true" \
--tenant-id '<tenantId-from-resolve-environment>'
构建与清单提供的 { logicalName, displayName, columns: [...] } 相同的结构。
第 3 步 — 选择表格 + 计数
仅评估经过验证的种子允许列表;仅当复用的表格明确位于该列表中时,才将其纳入评估。对于独立发现,只使用建议保留的候选表格,并在插入前获得选择批准。
未经下方额外的共享环境明确确认,绝不为标准系统表(例如 contact、account、systemuser)生成种子数据。
预生成种子数据前的行数检查(严格要求 — 每张表生成任何行之前都要执行):
对于每张范围内的表,使用清单中的已验证实体集名称查询当前记录数,或按照第 5a 步从元数据中获取该名称。
绝不能通过在逻辑名称后追加 s 来推导该名称。
cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
node "${PLUGIN_ROOT}/scripts/dataverse-request.js" <envUrl> GET \
"<entitySetName>?\$top=5&\$select=<primaryKeyColumn>" \
--tenant-id '<tenantId-from-resolve-environment>'
统计 value 数组中返回的行数。
| 现有记录数 | 操作 |
|---|---|
| ≥5 | 完全跳过此表。 记录:↷ <table> (≥5 records exist, skipping)。不要生成或插入任何行。 |
| <5 | 生成足够的新行,以达到每类目标数量。如果已有部分记录(例如 2 条),只生成差额(例如再生成 3 条,达到 5 条)。 |
如果所有符合条件的已选表格都已有 ≥5 条记录,则报告已批准的种子范围内无需生成任何种子数据,并停止。
按类别设置每张表的记录数(生成数据前,依据清单信号对每张表进行分类;这优于统一设为 5,因为参考表不需要大量数据,而事务表需要覆盖不同状态):
| 类别 | 启发式判断 | 默认数量 | 原因 |
|---|---|---|---|
| 参考表 | 没有 status / state 列;列为描述性字段(名称、地址、电话)。通常为第 0 层。 | 3 | 门店、客户、站点、产品。数量少且稳定。 |
| 联结表 | 有两个或更多查找列,没有其他有意义的列。通常为第 1 层。 | 每个父记录 3 条,最多 9 条 | Store-Assignment、Project-User。需要覆盖联结关系。 |
| 事务表 | 有 status / state / phase 选项列,以及日期列(createdon、submittedat、completedat)。 | 5 | 审计、检查、订单、工单。需要混合状态,以便磁贴显示内容。 |
| 明细 / 行项目表 | 有指向事务父记录的查找列,没有自身的状态列。 | 每个父记录 2-3 条 | 审计区域、订单行项目、检查发现。 |
| 问题 / 发现表 | 有指向事务父记录的查找列 + status(Open / Resolved)和严重程度(Critical / Moderate / Minor)。 | 每个事务父记录 3-4 条 | 问题、缺陷、观察项。混合严重程度和状态(见第 4a 步)。 |
| 证据 / 附件表 | 有 File / Image 列 + 指向问题 / 检查记录的查找列。 | 约每 30% 的父记录 1 条 | 照片证据、文档上传。默认生成元数据行;仅当 brand/media-policy.md 或用户请求表明需要示例媒体时,才生成文件 / 图像字节。 |
| 日志 / 事件 / 审计跟踪表 | 仅追加,包含 eventtype 枚举 + 时间戳 + 操作者查找列。 | 每个事务父记录 2 条 | 审计日志事件、活动流。每个父记录至少混合 2 种事件类型。 |
| 覆盖 / 审批表 | 有指向事务父记录的查找列 + 状态(Pending / Approved / Rejected)。 | 约每 20% 的父记录 1 条 | 覆盖请求、审批队列。至少添加 1 条 Pending 记录,让队列选项卡显示内容。 |
数量刻意保持精简。目标是让每个屏幕都能显示一些内容,而不是创建演示数据集。
打印一行摘要并继续:
→ Seeding <total> records into <N> tables (coverage-first; counts auto-tuned per class).
步骤 3b — 确定插入顺序
对于选定的表,根据查找列构建依赖图:
- 没有指向其他表的查找列 → 第 0 层(最先插入)
- 仅查找第 0 层表的表 → 第 1 层
- 继续分层,直到所有选定的表都已分层
如果选定的表引用了未选定的父表,则通过有界读取复用已验证的现有父记录,前提是该查找在已批准的功能范围内。读取查找父记录并不意味着可以插入或更新该记录。如果没有合适的记录,则向负责人返回 NEEDS_CONTEXT(或在独立执行时询问),以扩展种子范围或明确省略可选查找。绝不可自动将父表加入 seedTables、创建即将停用的父记录,或省略必需查找。指向即将停用目标的查找会阻止相关种子数据,直到计划得到协调。
步骤 4 — 生成示例数据 + 预览
遥测检查点:generate_and_review_sample_records
步骤 4a — 生成符合上下文的行
为每个选定的表生成 N 行。根据列名和类型匹配值:
| 列类型 | 生成方式 |
|---|---|
| String | 根据列名的语义匹配。*name、*title → 根据需求简介上下文生成真实感姓名。*email → [email protected]。*phone → (555) 123-NNNN。*address → 真实感街道 + 城市。其他情况:简短且符合上下文的文本。 |
| Memo(多行文本) | 生成 1-3 个与列名相关的句子(例如 *notes、*description)。 |
| Integer / Decimal / Currency | 根据列名选择合理范围。*amount、*price → 真实感美元金额。*count、*quantity → 小整数(1-100)。 |
| DateTime / DateOnly | 生成近期 ISO 日期,范围为过去 30 天至未来 14 天。各行日期应有所不同。 |
| Boolean | 混合生成 true/false(约 70/30;对于 is_active 这类名称,优先生成 true)。 |
| Choice (Picklist) | 先查询选项(步骤 4b),然后从有效整数值中选择。 |
| MultiSelect Choice | 每行从选项集里选择 1-3 个有效值。 |
| Lookup | 引用已验证的现有父记录,或在已批准的种子范围内插入的父记录。跟踪线上父记录 GUID;绝不可仅为满足子记录而播种未经批准的父记录。 |
| Image / File | 默认:跳过,留空。如果启用了媒体播种,且该列存储业务数据(产品图片、检查证据、NC 证明),则使用 assets/sample-* 中生成的/合成的本地文件,并记录来源。绝不可将装饰性的 UI 主视觉素材上传到 Dataverse。 |
媒体播种策略(仅限业务数据,且仅在需要时):
- 默认情况下,不播种二进制媒体。播种元数据行,并将 Image/File 列留空,除非屏幕计划或用户请求要求提供可见的示例媒体。
- 仅当图片属于用户会在列表/详情屏幕中查看的记录时,才向 Dataverse 播种图片/文件:产品照片、证据、附件、签名、问题证明。不要播种 Home 主视觉素材、启动画面、应用图标、空状态插画或装饰性的详情背景。
- 优先使用不含徽标、真实产品标签、人脸、水印和竞争对手品牌的生成素材/合成素材。如果用户提供了已批准的素材,则使用这些素材并记录来源。
- CDN URL 仅适用于明确的 URL/Text 列(例如
imageurl、photourl)。不要将 CDN URL 放入 File/Image 列。 - Dataverse Image 列在行负载或生成的服务形状中接收 base64。Dataverse File 列需要在元数据行创建后再执行一次上传。
- 对于产品/渠道应用,如果产品列表/详情屏幕需要显示图片,产品图片就是核心示例数据,但数量有限。仅生成并上传具有代表性的子集;其余记录使用本地占位图或将图片字段留空。
- 使用
assets/images/asset-manifest.json或assets/sample-media/asset-manifest.json维护文件、用途、来源/许可和安全说明。
媒体数量上限(硬性):
- 产品/目录表:默认最多为 min(6, record count) 条记录上传图片。如果记录按类别划分,则每个类别选择 1-2 条,直到达到上限。其余记录使用本地占位缩略图或空状态回退方案。
- 证据/附件表:最多为 30% 的父记录上传示例文件,每张表总计不超过 5 个文件。即使没有文件字节数据,元数据行仍可存在。
- 用户/头像/设备/站点图片:每张表最多上传 3 张,除非用户明确要求更丰富的视觉演示。
- 未经用户明确批准,在一次
/add-sample-data运行中,生成的媒体文件总数不得超过 10 个。 - 如果计算出的示例行数超过媒体上限,应优先保证覆盖面,而不是追求数量:一个主打产品、一个次要产品、一个边缘/状态示例,然后使用占位图。
如何插入示例图片:
- 先生成常规记录内容(名称、查找字段、状态等)。
- 对于 Image 列,只有当生成的模型/服务要求该列使用 base64 时,才在 create/PATCH 请求体中附加紧凑的 base64 数据。图片应足够小,以适合移动端缩略图。
- 对于 File 列,不要在 create 请求体中放入字节数据/base64/URL。先插入元数据行,从 Step 5 的
BATCH-RECORDS结果中获取 GUID,然后在 Step 5d 中将生成的文件上传到(recordId, columnName)。 - 达到媒体上限后停止。其余记录的 Image/File 列保持为 null,并在应用 UI 中使用本地占位图。
- 如果 File 列没有上传辅助工具,则将该列留空,并报告
sample media skipped — upload helper missing。绝不要用 URL 字符串伪造 File/Image 数据。
从需求简述中获取上下文。 用户描述了应用的用途(例如“供现场技术人员使用的 HVAC 检查应用”);应据此为数据增添特色,例如将站点命名为符合用户行业特点的街道名称,并使用恰当的状态术语。使用通用的 Lorem Ipsum 就是失败。
每个父记录的扇出下限(硬性要求)。 对于 Tier K+ 中的每张子表,除非关系明确为可选(清单中 RequiredLevel: None,且列名不暗示一对多关系,如 *audit*、*inspection*、*order*),否则每条 Tier K-1 父记录至少生成 1 行。如果没有这个下限,随机分配查找字段会导致某些父记录没有子记录,从而使父记录的详情屏幕显示为空。具体规则:如果正在生成 audit_zones,且有 5 条审计记录,则至少生成 5 个区域(每条审计记录 1 个),然后为每条审计记录再增加 0-2 个,直到达到每类记录的目标数量。绝不能反过来——绝不能只生成总计 N 条,再任由随机结果决定每行归属哪个父记录。
状态分布(对事务、问题、覆盖类为硬性要求)。 如果某张表有 status / state / phase / severity / priority 选项列,行数据必须分布在至少 2 个不同值中——不能全部为 Open,也不能全部为 InProgress。具体规则:
- 事务(
audit、inspection、order):如果选项集中有可用状态,至少混用 3 种生命周期状态——通常是 1 行处于早期状态(Draft/InProgress),1 行处于中期状态(Submitted/PendingReview),其余处于终态(Signed/Completed/Closed)。如果只有 2 种状态,则按约 60/40 的比例拆分。 - 问题/发现项:严重程度和状态应分别混合分布。如果有 5 个问题,涉及 3 种严重程度 × 2 种状态,应力求每种严重程度至少出现 1 次,并且至少有 1 个
Open和 1 个Resolved。 - 覆盖/审批:队列/收件箱标签页必须至少有 1 行处于驱动队列的状态(
Pending),其余记录分布在Approved/Rejected中。 - 日志/事件:每个父记录至少有 2 种不同的
eventtype值(例如Created+StatusChanged)。 日期分布(仅事务记录/日志)。 如果表中有createdon/submittedat/completedat/eventtimestamp列,请将记录分散到今天和过去 14 天(不要全部放在今天,也不要全部放在 30 天前)。分布:约 30% 在今天/昨天(用于展示“近期活动”图块),约 50% 在过去 7 天,约 20% 在 8–14 天前。参考表和详情表可以使用任意合理的日期——只有事务记录/日志需要按时间分布。
场景类型边界覆盖(可检测时必须满足)。 使用所选 Power Apps 场景类型,确保至少有一行体现应用 UI 承诺支持的每种关键状态:
- 现场检查/审计:一条受阻或缺少证据的审计、一条进行中的审计、一条已完成/已签署的审计。
- 资产维护:一张逾期/高优先级工单、一张等待零件的工单、一张已完成的工单。
- 库存扫描优先:一条差异、一件零库存/低库存商品、一条已解决的盘点/调拨记录。
- 审批:审批收件箱中有一项待处理、一项已批准、一项已拒绝/要求修改。
- CRM:一段有风险的关系、一项即将进行的跟进、一段健康/已赢单的关系。
- 零售目录/订单:一件带图片的商品、一件低库存/缺货商品、一张包含多个明细行的订单。
- 案例管理:一条已升级/SLA 有风险的案例、一条等待客户回复的案例、一条已解决的案例。
- 入职/培训:一个逾期的必修模块、一个进行中的模块、一个已完成/已认证的模块。
- 费用/请求受理:一份缺少收据的草稿、一项待审批请求、一项已批准/已付款的请求。
- 健康/保健/护理:一项今天到期的护理任务、一项错过/待跟进的任务、一个已完成/进展正常的目标。
步骤 4b — 探索 Choice 选项
对于所选表中的每个 Choice 列,在生成记录之前查询其选项集:
cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
node "${PLUGIN_ROOT}/scripts/dataverse-request.js" <envUrl> GET \
"EntityDefinitions(LogicalName='<table>')/Attributes(LogicalName='<column>')/Microsoft.Dynamics.CRM.PicklistAttributeMetadata?\$expand=OptionSet" \
--tenant-id '<tenantId-from-resolve-environment>'
使用响应中的实际 Value 整数。不要硬编码 100000000 这类值——它们会因环境而异。
步骤 4c — 向用户预览
对于每张表,直接在对话中用 Markdown 表格预览记录:
### Job Site (cr3e9_jobsite) — 5 records
| Site Name | Address | Square Feet | Active |
|---|---|---|---|
| Westside Construction Site | 4521 Industrial Pkwy | 12500 | true |
| Downtown Office Building | 188 Main St | 3200 | true |
| Eastgate Warehouse | 9047 Logistics Way | 28000 | false |
| Riverside Retail Plaza | 320 River Rd | 6800 | true |
| North Hills Distribution | 1612 Highland Ave | 18900 | true |
对于包含查找列的表,还要显示每条子记录引用的父记录:
cr3e9_inspection 行通过上方的 SiteName 引用 cr3e9_jobsite 记录。
步骤 4d — 继续插入
对于 --plan-only 或规划阶段的移交,请返回预览并在此处停止;
更早的批准或已填充的清单都不能越过此边界。
只有在当前限定范围内的移交或明确的独立请求中,确切的种子表/数量策略以及任何媒体写入都已获批准时,才可不经再次询问直接继续。否则,须先取得对预览的明确批准。行数用于防止重复数据量;它们不代表同意。取消后即停止。如果依赖项或记录需要扩大范围,请交还负责人处理,而不是在编排中的叶节点内批准扩大范围。
步骤 5 — 插入示例数据
遥测检查点:insert_sample_records
开始前打印:
"→ 正在按依赖层级顺序向 张表插入共 条记录(每层并行,最多同时处理 5 项)…"
按步骤 3c 中的层级顺序插入。在同一层级内,通过脚本的 BATCH-RECORDS 模式并行处理表和记录——使用单个 Node 进程,通过 Promise.all 将并发上限设为 5。不同层级之间保持串行(子记录需要从前一层级的响应中获取父记录 GUID)。
⚠️ 并行处理仅适用于记录插入。 架构操作(创建表/列/关系)必须保持串行——请参见
/add-dataverse步骤 5 并发规则。记录不受元数据锁影响,但元数据写入会受影响。混淆两者会导致 429 /MetadataLockHeldException。
步骤 5a — 获取实体集名称
对于每张表,获取其 EntitySetName(URL 路径名称,通常为复数形式 — 例如 cr3e9_jobsites)。如果 .datamodel-manifest.json 中包含此信息,则从中读取;否则:
cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
node "${PLUGIN_ROOT}/scripts/dataverse-request.js" <envUrl> GET \
"EntityDefinitions(LogicalName='<table>')?\$select=EntitySetName" \
--tenant-id '<tenantId-from-resolve-environment>'
缓存结果。
⚠️ 解析每个查找关系两端的 EntitySetName。 你不仅需要插入目标表的实体集名称(BATCH-RECORDS 中的
entitySet字段),还需要@odata.bind值(步骤 5c)所引用的每个查找目标的实体集名称。实体集名称与逻辑名称相互独立 — Dataverse 的复数形式不规则,版本后缀也会原样保留。容易出错的示例:cr3e9_inspectionv3→cr3e9_inspectionv3s(而不是cr3e9_inspections);cr3e9_inquiry→cr3e9_inquiries;cr3e9_status→cr3e9_statuses。切勿将路径构造为<logicalName>s— 始终从 EntityDefinitions 中读取。
步骤 5b — 一次插入一个层级,在层级内并行处理
对于从 0 到 N 的每个层级:
在构造或提交批次之前,重新检查每个目标逻辑名称是否在
seedTables 和 retiringTables 中。解析其确切的实体集,并且
如果任何插入/更新操作的目标是未经批准或正在退役的表,则拒绝整个拟提交批次。在执行媒体 PATCH/上传以及每次重试之前,也应用相同的检查;超出范围的操作属于范围错误,不应作为记录失败继续处理。
-
构建操作数组。 收集此层级所有表中的每个 (entitySet, body) 对。为每个操作标记一个唯一的
index,用于将结果映射回对应的行标识。[ { "index": 0, "entitySet": "cr3e9_jobsites", "body": { "cr3e9_sitename": "Westside Construction Site", "cr3e9_address": "4521 Industrial Pkwy", "cr3e9_squarefeet": 12500, "cr3e9_active": true } }, { "index": 1, "entitySet": "cr3e9_jobsites", "body": { "cr3e9_sitename": "Downtown Office Building", "cr3e9_address": "188 Main St", "cr3e9_squarefeet": 3200, "cr3e9_active": true } }, { "index": 2, "entitySet": "cr3e9_inspectors", "body": { "cr3e9_fullname": "Jane Smith", "cr3e9_email": "[email protected]" } } // ... all rows for tables in this tier ]维护侧边映射
{ index → { table, rowSemantic } },以便将结果与跟踪信息对应起来。 -
每个层级调用一次 BATCH-RECORDS。 脚本会在内部处理并发、重试、GUID 提取和自适应节流:
cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; } node "${PLUGIN_ROOT}/scripts/dataverse-request.js" <envUrl> BATCH-RECORDS \ "Tier <N>" \ --operations '<json-from-step-1>' \ --concurrency 5 \ --solution '<solution-uniquename-from-memory-bank>' \ --tenant-id '<tenantId-from-resolve-environment>'输出为
{ "status": 200, "data": [{ "index": 0, "status": 204, "recordId": "<guid>" }, ...] }。结果按输入索引排序(并行执行期间顺序保持稳定)。 -
捕获父记录 GUID。 对于响应中每个成功的行,按行标识存储
recordId。第 N+1 层级的@odata.bind值来自此映射。每插入一条记录打印一行(按输入索引顺序遍历响应数组,以便输出按顺序显示):
→ ✓ <table>: <primary-name-value> (<guid>) -
处理失败。 如果层级中的任何操作返回非 2xx 状态或
error,则跳过父记录失败的子行,但继续处理下一层级,并使用已成功的行。原因:中止第 1 层会导致第 2 层及后续层级为空,无法满足覆盖范围约定。在步骤 6 的摘要中记录每个跳过的子行及其对后续层级的影响。
Step 5c — 使用查找列插入子表(也通过 BATCH-RECORDS)
对于 Tier 1 及以上的表,使用 @odata.bind 引用上一层级中捕获的父记录 GUID,构造每行的正文。BATCH-RECORDS 调用格式相同:
[
{
"index": 0,
"entitySet": "cr3e9_inspections",
"body": {
"cr3e9_inspectionnumber": "INS-001",
"cr3e9_status": 100000000,
"[email protected]": "/cr3e9_jobsites(<parent-guid-from-tier-0>)"
}
}
]
然后:
cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
node "${PLUGIN_ROOT}/scripts/dataverse-request.js" <envUrl> BATCH-RECORDS \
"Tier 1" \
--operations '<json-with-bound-lookups>' \
--concurrency 5 \
--solution '<solution-uniquename-from-memory-bank>' \
--tenant-id '<tenantId-from-resolve-environment>'
⚠️ 查找列写入语法(严格要求 — 格式错误时可能会静默保存为 null,也可能直接返回 404):
@odata.bind左侧的属性名称必须是 PascalCase 导航属性,例如cr3e9_Site(而非只读的_cr3e9_siteid_value)。- 值是实体集路径:
/<entitySetName>(<guid>)。注意前导的/。<entitySetName>是通过 Step 5a 获取的查找目标表的 EntitySetName,而不是在列的逻辑名称后附加s。 将其构造为<logicalName>s会返回Entity '<targetTable>' With Id = <guid> Does Not Exist,即使记录确实存在也是如此,因为 Dataverse 无法将错误的实体集解析到正确的表。带版本号的表(cr3e9_inspectionv3→cr3e9_inspectionv3s)和不规则复数形式(cr3e9_inquiry→cr3e9_inquiries)是常见陷阱。<guid>必须来自当前进程中的实时查询——可以来自父记录的 BATCH-RECORDS 响应(Step 5b/5c),也可以在恢复操作时通过对父表发起新的 GET 请求获取(Step 5f)。切勿复制粘贴先前会话标准输出中的 GUID。- 格式错误要么会静默丢弃关联关系(记录在没有父级 FK 的情况下写入,应用中显示为孤立记录),要么导致整个层级返回 404。规范格式请参阅
references/dataverse-reference.md§ Setting Lookups。
Step 5d — 插入记录后上传示例媒体(仅在启用时)
仅当 Step 4a 为业务 Image/File 列创建了媒体任务时才运行此步骤。切勿将其用于装饰性 UI 资源。
每个媒体任务的目标表仍须位于 seedTables 中,且不在
retiringTables 中;先前捕获的 GUID 不代表可以将其用作新目标。
生成行时构建一个 mediaJobs sidecar:
[
{
"table": "cr3e9_product",
"rowIndex": 2,
"recordId": "<filled-after-BATCH-RECORDS>",
"columnName": "cr3e9_productimage",
"columnType": "Image",
"filePath": "assets/sample-media/gatorade-aseptic.png",
"fileName": "gatorade-aseptic.png",
"purpose": "product photo"
}
]
每个层级的 BATCH-RECORDS 响应返回后,从内存中的 { index → recordId } 映射填入 recordId,然后按列类型上传媒体:
- 对每个媒体辅助程序(包括其身份验证和令牌刷新流程),都明确传入 Step 1 的环境 URL 和租户 ID。通过
dataverse-request.js发出的 Image PATCH 请求也必须使用相同的--tenant-id参数。 如果 File 上传辅助程序无法确保绑定到该租户,则不要调用它,也不要回退到环境凭据:保留未设置的字节,报告sample media skipped — tenant-bound upload helper missing,并返回保留元数据行的媒体问题。 - **Image 列:**如果创建正文中已包含 base64 值,则将媒体任务标记为已完成。否则,使用实体集和记录 GUID 将 base64 字符串 PATCH 到该记录。图像应保持较小且适用于应用;切勿将数 MB 的原始照片上传为种子数据。
- **File 列:**记录创建后调用受支持的上传辅助程序。在应用代码中,生成的服务形式为
Service.upload(recordId, columnName, file, fileDisplayName?);对于 CLI 种子数据填充,请使用执行相同 Dataverse 文件上传流程的专用辅助程序。仅支持 JSON 的dataverse-request.js BATCH-RECORDS模式不支持二进制上传。
**硬性失败规则:**如果示例行的媒体上传失败,请保留元数据行,在 Step 6 中记录媒体失败,并继续处理。不要删除该行或重新运行架构创建。应用仍应能使用占位图像回退来展示可用的列表和详情数据。
切勿执行以下操作:
- 除非列的类型确实是 URL/Text,否则切勿将 CDN URL 写入 File/Image 列。
- 切勿将 base64 填入 File 列。
- 切勿将 UI 主视觉图、启动图或图标资源上传到 Dataverse。
- 切勿猜测
columnName;应使用.datamodel-manifest.json/field_bindings.file_columns中的确切逻辑名称。
步骤 5e — 跟踪结果 + memory-bank
BATCH-RECORDS 响应是你的跟踪依据。对于每个层级的响应数组:
- 成功(
status为 200-299,且存在recordId)→ 追加到内存中的{ index → { table, primaryNameValue, recordId } }映射。 - 失败(
status为 0 / 4xx / 5xx,且带有error)→ 追加到各层级的失败列表,供步骤 6 汇总。
所有层级完成后(或严格默认模式停止后),批量写入 memory-bank.md 中的 ## Seeded sample data,以确保后续运行具有幂等性。每次运行中每个表占一行:
## Seeded sample data
| Date | Table | Records inserted | First GUID | Last GUID |
|---|---|---|---|---|
| <ISO> | cr3e9_jobsite | 5 | <guid1> | <guid5> |
运行结束后,在单次 Edit 调用中写入所有行,而不是逐层级写入——这样可避免迭代期间并发写入文件造成竞争,并确保审计日志具有原子性。
步骤 5f — 从失败的层级恢复 / 继续
如果某个层级中途失败,需要重试时,不要编写手工制作的 seed-resume.js,将之前运行的标准输出中的父级 GUID 写死其中。 这种做法是导致 Entity '<table>' With Id = <guid> Does Not Exist 404 错误的头号原因,因为 (a) 之前的运行可能实际上并未提交它所打印的父级行;(b) 重新创建的表所对应的 GUID 会立即过期。
唯一安全的恢复模式:
恢复前,重新确认当前获批的 seedTables 和退役排除项。之前 memory-bank 中的成功记录或种子文件,并不能授权使用已从当前范围移除的表;绝不要重试正在退役的表。
-
通过稳定的业务键重新查询父级 GUID。 对失败层级的
@odata.bind值中引用的每个父级表,使用该行的自然标识符(名称、尾号、代码——即播种时用作主名称的值)执行一次全新的 GET 筛选查询。示例:cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; } node "${PLUGIN_ROOT}/scripts/dataverse-request.js" <envUrl> GET \ "<parentEntitySet>?\$select=<parentIdColumn>,<naturalKey>&\$filter=startswith(<naturalKey>,'<seed prefix>')&\$top=50" \ --tenant-id '<tenantId-from-resolve-environment>'根据响应构建新的
{ naturalKey → guid }映射。 -
重新查询每个查找目标的 EntitySetName(步骤 5a)。不要信任之前会话中缓存的值——表的 EntitySetName 是稳定的,但缓存文件可能来自其他项目。
-
如果父级查询返回的行数少于预期,立即停止。 不要使用不完整的映射继续操作——将数量告知用户:
⛔ Resume aborted: expected <N> parent rows in <table>, found <K>. Re-run /add-sample-data from Tier 0 (don't trust the prior session's GUIDs). -
使用全新查询到的 GUID 和 EntitySetName 替换操作数组中的值, 按照常规步骤 5b/5c 发起
BATCH-RECORDS调用,重新提交失败的层级。使用相同的--solution标志、从已验证的步骤 1 身份信息中明确指定的--tenant-id,并遵守并发规则;绝不要为了恢复而切换租户。 -
继续执行后续层级, 使用本次恢复响应中的数据填充新的内存
{ index → recordId }映射。
为什么目前没有
--resume标志: 严格默认的失败模式(“停止该层级、显示失败情况,让用户重新运行”)与上述实时查询规则结合起来,就是恢复机制。标志会让人以为脚本能自动判断哪些数据已存在——事实并非如此,因为部分提交和并发编辑会使任何缓存都不可靠。恢复决策由用户负责;Agent 的职责是遵循实时查询模式,绝不臆造有状态的恢复辅助工具。
步骤 6 — 汇总
✅ Sample data seeded
─────────────────────────────────────────────
Environment : <envUrl>
Solution : <solution>
Seed scope : <approved targets; excluded/retiring targets>
Tables seeded : <list with counts, e.g. cr3e9_jobsite (5), cr3e9_inspection (5)>
Total records : <N>
Failures : <K> (see list below if K > 0)
Next steps:
npm run dev # Open the app — home screen now shows real data
/preview-screens # Generate static HTML preview with sample data
─────────────────────────────────────────────
如果有记录失败,请打印一张失败记录子表,并附上错误消息,方便用户诊断。
对于部分结果,返回非成功 / 存疑状态;绝不要声称未播种或已排除的表已成功填充。播种不会更改计划的架构,也不会从 .datamodel-manifest.json 中移除过渡条目。
硬性规则
- 仅覆盖种子允许列表中的表。 已弃用或未经批准的表始终排除,即使它们出现在清单中,或是扇出目标所必需的。遵守行数跳过规则和标准系统表确认要求;覆盖目标绝不能成为扩大种子范围的理由。
- 每个父项的扇出下限——Tier K-1 中的每个父项在 Tier K 中至少有 1 个子项(当该关系为必需关系或表示一对多时)。随机分配查找值会留下孤立的父项,并破坏父项-详情页面。应逐个父项生成子行,而不是将所有行作为一个批次随机选择父项。
- 事务、问题、覆盖和日志表的状态分布——至少混合 2 种不同的值;绝不能全部为
Open、全部为InProgress或全部为Pending。各类别的目标请参见 Step 4a。 - 事务表/日志表的日期分布——在今天及过去 14 天内分散分布,不要集中在某一天。约 30% 为近期、50% 为上周、20% 为更早日期。
- 实际插入时使用
BATCH-RECORDS模式——使用单个 Node 进程,在同一层内并行处理,按序返回结果,并在遇到 429 时自适应调整并发数。不要逐行调用node dataverse-request.js POST ...:每次冷启动约需 150-300ms,而批处理模式的目的正是摊薄这部分开销。 - 不同层之间始终顺序执行,同一层内始终并行执行。 子项需要父项 GUID;同一层中的行彼此独立。弄反了,要么会产生孤立的查找值(并行度过高),要么会浪费时间(顺序执行过多)。
- 并发上限为 5(
BATCH-RECORDS的默认值)。脚本在首次遇到 429 时会自动降至 3。除非你已测量并确认该环境的服务保护上限,否则不要将--concurrency设置得更高。 - 并行仅适用于记录插入。 架构操作(
EntityDefinitions、Attributes、RelationshipDefinitionsPOST 请求)必须保持顺序执行——Dataverse 元数据锁。现有/add-dataverseStep 5 并发规则不变。 - 默认情况下,绝不为标准系统表(
contact、account、incident等)生成种子数据——这些表与环境中的其他应用共享。如果用户明确选择这些表,须显示确认提示:“这些记录将对该环境中的所有应用可见。是否继续?” - 始终传入
--solution,确保记录归入我们的解决方案。 - 查找值使用
@odata.bind,其值格式为 PascalCase 导航属性名称加/entitySet(guid)。 其他格式要么会静默丢弃关系,要么返回 400,或以Entity '<target>' With Id = <guid> Does Not Exist返回 404。BATCH-RECORDS 模式会原样传递请求正文——它不会验证@odata.bind的格式,这由调用方负责。 - 查找目标的 EntitySetName 必须查询获取,绝不能猜测。
<logicalName>s对于带版本号的表(cr3e9_inspectionv3s)和不规则复数形式(cr3e9_inquiries、cr3e9_statuses)都是错误的。必须从 EntityDefinitions(Step 5a)中读取@odata.bind值所引用的每个查找目标的 EntitySetName。 @odata.bind中的 GUID 必须来自当前进程中的实时查询,绝不能使用之前会话的标准输出。 恢复运行时,根据自然键重新查询父项(Step 5e);绝不要把 GUID 粘贴到手工编写的seed-resume.js中。- 不要手工编写恢复脚本。 如果某一层失败,请遵循 Step 5e(重新查询,并使用常规 BATCH-RECORDS 调用重新提交失败的层)。包含硬编码
inspGuids = [...]的.tmp/seed-resume.js必定会导致 404。 - 选项列使用选项集中的实际数值
Value,而不是标签。 先查询选项集(Step 4b)。 - 失败时不要自动回滚。 严格默认模式(交互式)会在某一层失败后停止运行;自动模式/覆盖优先模式会继续执行并跳过孤立项。无论哪种情况,清理决定都由用户负责——绝不自动 DELETE 行。
参考
- microsoft/power-platform-skills/power-pages/add-sample-data — 本技能参考的技能
skills/add-dataverse/references/dataverse-reference.md— 设置查找字段、选项字段和格式化值scripts/dataverse-request.js— 捆绑的 HTTP 辅助工具,支持--solution、--include-headers、--body标志