关于 setup-solution
setup-solution 是面向 Power Pages 项目的 ALM 技能,用于在 Dataverse 中创建发布者和解决方案,并将站点组件加入解决方案。它还生成供后续导出、导入和流水线技能使用的清单。
主要功能
- 01创建 Dataverse 发布者和解决方案
- 02将 Power Pages 组件加入解决方案
- 03支持现有解决方案同步
- 04生成解决方案清单
- 05支持多解决方案计划
适用场景
- 01为 Power Pages 项目建立可部署的解决方案
- 02将现有解决方案与站点组件清单同步
- 03按拆分计划创建多个解决方案
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ microsoft/ power-platform-skills --skill setup-solution安装后在 Claude Code 中输入/setup-solution确认已加载。
附带文件scripts/validate-solution.js
详细介绍
插件检查:运行
node "${PLUGIN_ROOT}/scripts/check-version.js"— 如果输出消息,请在继续之前展示给用户。
setup-solution
创建 Dataverse 发布者和解决方案,然后添加 Power Pages 站点组件。写入 .solution-manifest.json,供 export-solution、import-solution 和 setup-pipeline 技能使用。
前置条件
- 已安装 PAC CLI 并完成身份验证(
pac env who返回环境 URL) - 已安装 Azure CLI 并登录(
az account show成功) - 项目根目录中存在
powerpages.config.json(站点必须至少部署过一次,以便.powerpages-site/中存在组件记录)
阶段
阶段 0 — ALM 计划门禁
plan-alm是入口。 当用户表达 ALM 意图(提升 / 发布 / 部署 / 设置 CI-CD / 移至暂存环境 / 推送到生产环境)时,编排器(/power-pages:plan-alm)应首先运行。此阶段 0 会强制执行这一要求,并且在没有计划时默认拒绝继续,而不是一次性检查后就允许用户永远忽略。
跳过规则。 如果此技能是作为正在执行的 plan-alm 编排的一部分调用的,则完全跳过阶段 0,继续阶段 1。门禁辅助程序通过其 inExecution 区块提供此信息 — 出现以下情况时静默跳转到阶段 1:
inExecution.status === "active"
辅助程序根据 docs/.alm-plan-data.json 计算此状态 — PLAN_STATUS === "In Execution",且 LAST_INVOCATION_AT 在最近 60 分钟内。每次 check-alm-plan.js 调用时,只要发现计划正在执行,就会自动刷新 LAST_INVOCATION_AT,因此链中的每个技能都会让调用链持续有效,以便执行下一个技能 — 即使是多小时的部署(deploy-pipeline 的每个阶段单独就可能需要 60 分钟),也能在此时间窗口内完成,而不会导致调用链被错误地判定为不再执行。若调用链停滞(超过 60 分钟没有心跳),则会重新归类为 stale-heartbeat,并正常触发阶段 0 门禁,避免已放弃的计划在没有用户确认的情况下被静默绕过。
当 inExecution.status 不为 "active"("not-running"、"stale-heartbeat"、"no-plan")时,运行以下阶段 0 门禁流程。根据辅助程序返回的其他字段进行分支处理:
步骤 1 — 运行门禁辅助程序。
node "${PLUGIN_ROOT}/scripts/lib/check-alm-plan.js" --projectRoot "."
辅助程序返回包含 { exists, deferred, stale, staleness: { reason, detail }, generatedAt, planStatus, ... } 的 JSON。同步模式(.solution-manifest.json 已存在时)可能还会在阶段 1 获取相关信息后传入 --envUrl、--token、--solutionId,但初始门禁只需检查是否存在计划。
步骤 2 — 根据结果分支处理。
| 结果 | 行为 |
|---|---|
deferred: true | 用户已明确将此项目的 ALM 推迟(存在 .alm-deferred 标记)。静默跳转到阶段 1 — 不要反复提醒。 |
exists: false | 用户尚未运行 plan-alm。请参见步骤 3。 |
exists: true, stale: false | 计划是最新的。静默跳转到阶段 1。 |
exists: true, stale: true(原因:solution-modified) | 解决方案在生成计划后发生了更改。请参见步骤 4。 |
步骤 3 — 没有计划。 告诉用户:
"此项目不存在 ALM 计划。
/power-pages:plan-alm会生成计划 — 它会检测项目状态,询问你的提升策略(PP Pipelines 还是手动导出/导入),并按正确顺序编排适当的技能(包括此技能)。要我现在运行 plan-alm 吗?"
🚦 门禁(意图 · setup-solution:0.no-plan): 当
check-alm-plan.js返回exists:false时,默认拒绝继续的入口门禁。由辅助脚本支持。
AskUserQuestion:
| 问题 | 标题 | 选项 |
|---|---|---|
先运行 /power-pages:plan-alm? | ALM 计划门禁 | 是 — 现在运行 /power-pages:plan-alm(推荐),不制定计划直接继续(高级 — 我知道自己在做什么),取消 |
- 是(推荐) → 调用
/power-pages:plan-alm。它会构建计划并返回 —plan-alm是规划器,不会部署。本技能随后会重新运行 Phase 0 检查(此时结果为exists:true),并继续执行 Phase 1。 - 不使用计划,继续 → 设置
BYPASSED_PLAN_GATE = true,并继续执行 Phase 1。 - 取消 → 正常退出。
步骤 4 — 计划已过期。 告知用户:
"ALM 计划生成于
{generatedAt},但源解决方案已在此后修改(修改时间为{solution.modifiedon})。组件可能已发生变化。重新运行plan-alm将刷新分析结果和生成的 HTML。"
🚦 门控(意图 · setup-solution:0.stale-plan): 当
check-alm-plan.js返回stale:true时,执行失败即关闭的入口门控。由辅助脚本支持。
AskUserQuestion:
| 问题 | 标题 | 选项 |
|---|---|---|
| 先刷新计划? | ALM 计划时效性 | 刷新 — 重新运行 /power-pages:plan-alm(推荐),继续使用现有计划,取消 |
- 刷新(推荐) → 调用
/power-pages:plan-alm。完成后,重新运行一次 Phase 0 辅助程序以确认计划是否最新;如果仍已过期,显示详细信息并继续执行 Phase 1(不要无限循环)。 - 继续 → 设置
STALE_PLAN_ACK = true,并继续执行 Phase 1。 - 取消 → 正常退出。
此门控的存在原因。 直接调用 setup-solution 会构建(或同步)解决方案,而不会参考编排器的计划。如果计划已存在,且建议拆分为多个解决方案,那么单独运行此技能可能会将组件整合到错误的基础解决方案中。如果尚无计划,plan-alm 原本会在创建任何解决方案之前,提示拆分建议、资产大小提示以及缺失组件。如果先运行 setup-solution,这些决策就会在不知情的情况下被直接略过。此门控可确保在合适的上下文中运行 setup-solution,同时仍为确实需要一次性解决方案的用户提供明确的跳过选项。
Phase 1 — 验证前提条件
在此阶段开始时一次性创建所有任务。
要创建的任务:
- “验证前提条件”
- “收集解决方案配置”
- “检查现有发布者和解决方案”
- “创建发布者和解决方案”
- “将站点组件添加到解决方案”
- “验证并写入清单”
- “提供摘要”
步骤:
-
运行
pac env who— 提取environmentUrl、organizationId(向用户显示以供确认) -
运行
verify-alm-prerequisites.js以确认 PAC CLI 身份验证、获取令牌并验证 API 访问权限:node "${PLUGIN_ROOT}/scripts/lib/verify-alm-prerequisites.js" --envUrl "{environmentUrl}"将输出捕获为 JSON;提取
.envUrl(存储为envUrl)和.token(存储为token)。如果脚本以非零状态退出,请停止并说明缺少什么(参考${PLUGIN_ROOT}/references/dataverse-prerequisites.md)。 -
定位
powerpages.config.json— 读取siteName和websiteRecordId -
确认
.powerpages-site/文件夹存在(查找组件记录时必需) -
检查 ALM 计划上下文 — 查找
docs/alm/alm-plan-context.json:🚦 门控(计划 · setup-solution:1.preloaded): 使用预加载的计划分类,或重新发现。在选择之前不会执行任何写入操作。
- 如果找到,请通过
AskUserQuestion询问:“此前已为此网站生成 ALM 计划,其中包含预先分类的网站设置列表(keepAsIs、promoteToEnvVar、authNoValue、excluded)。您想使用这些选项,还是现在重新发现并重新分类所有内容?”
- 选项:“使用计划中的预加载选项” / “重新发现并重新分类”
- 如果用户选择预加载选项:读取
docs/alm/alm-plan-context.json,将siteSettings对象存储为preloadedSettings。到达步骤 5.3 时,跳过查询和分类逻辑,直接使用preloadedSettings。 - 如果用户选择重新发现:按正常流程继续(步骤 5.3–5.4 查询 Dataverse 并重新分类)。
- 如果找到,请通过
-
检测同步模式 — 检查项目根目录中是否存在
.solution-manifest.json。-
如果存在:读取该文件,并通过
GET {envUrl}/api/data/v9.2/solutions({solutionId})?$select=solutionid,uniquename,version,ismanaged验证目标环境中是否仍存在solutionId。- 如果该解决方案仍存在且在此环境中为非托管:设置
syncMode = true,并将existingSolution存储为清单内容。
🚦 关卡(同意 · setup-solution:1.stale-manifest): 清单引用的解决方案在当前环境中不存在。请从头开始(备份清单并创建新解决方案)或中止。
- 如果未找到该解决方案、该解决方案为托管,或该解决方案位于其他环境:将其视为过期清单,告知用户,并通过
AskUserQuestion询问:“现有
.solution-manifest.json指向解决方案{uniqueName}v{version},但我在当前环境中找不到它。您想:1)从头开始(备份清单并创建新解决方案);2)中止,以便您进行调查?” 只有在用户明确选择后才能继续。
- 如果该解决方案仍存在且在此环境中为非托管:设置
-
如果不存在:设置
syncMode = false— 这是全新设置。
-
-
向用户报告所选模式:
syncMode = true:“找到现有解决方案{uniqueName}v{version}。正在以同步模式运行 — 我将发现当前网站清单,与解决方案中已有的内容进行比较,并仅添加缺失的组件。”syncMode = false:“未找到现有解决方案清单。正在进行全新设置 — 我将创建发布者和解决方案,然后添加所有网站组件。”
-
检查拆分计划(多解决方案模式) — 查找
docs/alm/alm-split-plan.json(由plan-alm阶段 1 步骤 10 写入):- 如果找到且
proposedSolutions.length > 1,设置MULTI_SOLUTION_MODE = true,并将该数组存储为PROPOSED_SOLUTIONS。 - 在多解决方案模式下:
- 阶段 2 只询问一次发布者详细信息(所有解决方案共享),并提供建议的解决方案名称/版本供用户确认(用户可在继续前逐一修改)。
- 阶段 4 首先创建发布者(单个串行步骤 — 每个解决方案都绑定到该发布者),然后并行创建
PROPOSED_SOLUTIONS中的解决方案。order字段是下游流水线阶段排序所用的数据 — 它不会限制创建顺序,因为每个解决方案彼此独立(uniqueName各不相同,共享publisherId,且解决方案之间没有依赖关系)。 - 阶段 5 根据
proposedSolutions[i].componentTypes和tableLogicalNames(针对策略 3)为每个解决方案划分AddSolutionComponent调用。 - 阶段 6 写入清单 v2(见下文)。
- 如果未找到拆分计划或
proposedSolutions.length === 1,则按单解决方案模式继续(现有流程)。
- 如果找到且
阶段 1.5 — 根据当前 ALM 文档确认信息
参考:
${PLUGIN_ROOT}/references/alm-docs-grounding.md
此步骤最多用时约 30 秒。如果 MCP 搜索/获取出错,记录一条简短说明并继续 — 此技能必须能够离线运行。
- 使用查询
Power Pages solution publisher creation Dataverse component types ALM运行microsoft_docs_search。 - 通过
microsoft_docs_fetch并行获取https://learn.microsoft.com/en-us/power-platform/alm/solution-concepts-alm(如果搜索结果显示有相关的新教程,最多再获取一个相关页面 — 例如,多解决方案分层、托管属性指南)。 - 提取一段摘要,说明 Microsoft Learn 当前关于解决方案组件、发布者前缀不可变性、托管与非托管的选择,以及组件类型整数值的说明。与
${PLUGIN_ROOT}/references/solution-api-patterns.md进行比较,并标记任何差异(新增组件类型、操作签名变更、已弃用的模式)。 - 使用此摘要为阶段 2 及后续阶段的决策提供依据。不要悄悄更改技能行为 — 在阶段 4(创建发布者和解决方案)之前,向用户提示任何差异,但不强制中断流程。
阶段 2 — 收集解决方案配置
当
syncMode = true时,跳过整个阶段。 改用清单中的existingSolution.publisher和existingSolution.solution。跳转至阶段 5。
🚦 关卡(征求同意 · setup-solution:2.publisher-prefix): 发布者前缀是永久的,并会添加到每个组件逻辑名称的前面。必须获得明确确认。取消操作会在写入任何发布者/解决方案信息之前退出。
通过 AskUserQuestion 询问用户:
- 发布者唯一名称(例如
contoso)— 仅限小写字母/数字,不含空格。说明此名称是永久的,无法更改。 - 发布者显示名称(例如
Contoso)— 显示名称 - 发布者前缀(例如
con)— 2–8 个小写字母,会添加到所有组件前面。说明此名称是永久的,无法更改。 - 解决方案唯一名称(例如
ContosoSite)— 仅限字母/数字/下划线,不含空格 - 解决方案显示名称(例如
Contoso Site)— 显示名称 - 解决方案版本(默认值:
1.0.0.0)— 必须采用major.minor.build.revision格式
展示所有值的确认摘要,并等待用户批准后再继续。
关键决策点:发布者前缀和发布者唯一名称都不可撤销 — 继续之前暂停并明确征求用户确认。
阶段 3 — 检查现有状态
当
syncMode = true时,跳过整个阶段。 清单保证解决方案存在,并且我们已在阶段 1 步骤 6 中完成验证。
创建任何内容之前,检查发布者和解决方案是否已存在:
- 查询发布者:
GET {envUrl}/api/data/v9.2/publishers?$filter=uniquename eq '{publisherUniqueName}'&$select=publisherid,uniquename,customizationprefix(没有专用的发布者脚本 — 直接查询 OData 终结点。) - 使用
verify-solution-exists.js检查解决方案是否存在:
将输出捕获为 JSON;检查node "${PLUGIN_ROOT}/scripts/lib/verify-solution-exists.js" \ --envUrl "{envUrl}" \ --uniqueName "{solutionUniqueName}" \ --token "{token}".found(布尔值)。如果found为真,还读取.solutionId、.version和.isManaged以供显示。
向用户报告结果:
- 如果发布者存在:"找到现有发布者
{name}(前缀:{prefix})。将重用该发布者。" - 如果解决方案存在:"找到现有解决方案
{name},版本为{version}。将重用该解决方案并添加组件。" - 如果两者都不存在:"将创建新的发布者和解决方案。"
等待用户确认后再继续。
阶段 4 — 创建发布者和解决方案
当
syncMode = true时,跳过整个阶段。 发布者和解决方案已存在。同步模式下提升版本号:在第 5 阶段执行任何添加操作之前,提升现有解决方案的补丁版本段,确保同步后的清单和后续导出能正确取代先前版本。使用共享助手脚本——它是版本提升规则的唯一事实来源(缺失段补零、整数数值处理
1.0.0.9 → 1.0.0.10、拒绝1.0.0.a、拒绝超过 4 段的版本)。export-solution第 4 阶段第 4.0 步也会调用同一助手脚本——对于相同的输入版本,这两个技能必须产生相同的版本提升结果。node "${PLUGIN_ROOT}/scripts/lib/bump-solution-version.js" \ --envUrl "{envUrl}" \ --token "{token}" \ --solutionId "{solutionId}" \ --projectRoot "."将输出捕获为 JSON;助手脚本返回
{ previous, next, bumped: true, manifestUpdated, manifestUpdateReason }。传入--projectRoot "."后,助手脚本可自动更新.solution-manifest.json中的solution.version(单解决方案)或匹配的solutions[].version(多解决方案)字段——如果不传入该参数,清单会在每次版本提升后落后于实际版本。将本地的existingSolution.solution.version更新为.next,确保最终写入清单时包含提升后的版本。在第 5.6 步添加组件之前完成此操作,这样即使技能中途被中断,清单也能保持一致。不要内联 PATCH 请求——防止此技能与export-solution之间的规则产生分歧,正是创建该助手脚本的目的。
有关准确的请求正文模板,请参阅 ${PLUGIN_ROOT}/references/solution-api-patterns.md。
-
创建发布者(如果尚不存在):
- 使用发布者正文向
POST {envUrl}/api/data/v9.2/publishers发送请求 - 从
OData-EntityId响应标头中提取publisherId - 如果失败:报告错误并停止(不要继续创建解决方案)
- 此步骤必须在创建任何解决方案之前完成——每个解决方案正文都会绑定
[email protected]。此步骤必须串行执行,不得并行。
- 使用发布者正文向
-
创建解决方案:
单解决方案模式(
MULTI_SOLUTION_MODE = false)——调用create-solution.js。省略--token,以便助手脚本在调用时通过getAuthToken(envUrl)刷新令牌(传入可能已过期的缓存令牌会导致 401,而助手脚本不会重试):node "${PLUGIN_ROOT}/scripts/lib/create-solution.js" \ --envUrl "{envUrl}" \ --uniqueName "{solutionUniqueName}" \ --friendlyName "{solutionFriendlyName}" \ --version "{version}" \ --publisherId "{publisherId}" \ --description "Power Pages solution for {siteName}"将输出捕获为 JSON;提取
.solutionId(存储为solutionId)。如果失败(退出码非零或created: false):报告错误并停止。多解决方案模式(
MULTI_SOLUTION_MODE = true)——调用create-solutions-batch.js,该脚本通过Promise.allSettled并行处理所有PROPOSED_SOLUTIONS(通常 5–6 个解决方案拆分约 2 秒即可完成,而串行执行约需 10 秒)。助手脚本会自动跳过isFutureBuffer: true的条目(保留的缓冲区会在用户实际添加新组件时创建),并通过verify-solution-exists.js幂等地处理 409 冲突。将规格写入临时 JSON 文件,然后调用:node -e "require('fs').writeFileSync('./docs/alm/.solutions-batch.json', JSON.stringify({{PROPOSED_SOLUTIONS_AS_SPECS}}))" node "${PLUGIN_ROOT}/scripts/lib/create-solutions-batch.js" \ --envUrl "{envUrl}" \ --token "{token}" \ --publisherId "{publisherId}" \ --solutionsFile ./docs/alm/.solutions-batch.json其中,
{{PROPOSED_SOLUTIONS_AS_SPECS}}是将PROPOSED_SOLUTIONS映射为每个条目的{ uniqueName, friendlyName: displayName, version: "1.0.0.0", description, isFutureBuffer }(保留isFutureBuffer标记,以便助手脚本跳过该条目)。将输出捕获为 JSON;根据result.results构建SOLUTIONS_BY_NAME = { uniqueName → { solutionId, created } }(不添加包含skipped: true的条目——Future缓冲区解决方案尚不存在于 Dataverse 中)。如果result.failed > 0,显示每个条目的error字符串并停止——已成功创建的解决方案仍会保留在 Dataverse 中,用户可在同步模式下重新运行 setup-solution 以恢复。调用后删除临时文件(./docs/alm/.solutions-batch.json)。批处理前必须确保令牌有效——如果未传入
--token,create-solutions-batch.js会在启动时通过getAuthToken(envUrl)刷新一次,因此优先省略--token,不要传入过期令牌。 -
报告:“发布者
{name}已就绪。已创建{N}个解决方案:{name1}、{name2}、……”(单解决方案模式下,仅报告该解决方案)。
阶段 5 — 添加站点组件
有关 AddSolutionComponent 请求正文模板和 powerpagecomponents 发现模式,请参阅 ${PLUGIN_ROOT}/references/solution-api-patterns.md。
同步模式行为:当
syncMode = true时,运行发现辅助脚本并填入--solutionId,使用返回的missing.*数组作为候选集。本阶段的其他所有操作(5.1 中的动态组件类型查找、5.3 中的分类、5.4 中的 OAuth 密钥转换、5.4b 中的环境变量采纳、5.4c 中的孤立 ppc 采纳、5.5 中的清单摘要、5.6 中的批量添加)都以相同方式运行,只是使用预先筛选过的“尚未加入解决方案的内容”列表。同步模式的目标是:用户在上次运行setup-solution后添加了服务器逻辑、机器人、流、环境变量或页面时,可以重新调用此技能,让这些组件被采纳,而无需进行任何全新设置提示。全新模式行为(
syncMode = false):按下文所述执行完整发现——将每个 ppc、每种站点语言、每个自定义表以及每个发布者前缀环境变量都列为候选项。
步骤 5.1 — 动态发现组件类型
不要硬编码组件类型编号。 组件类型代码是特定于环境的元数据,并且因租户而异。务必使用 discover-component-types.js 在运行时解析。
使用网站记录 ID、一个示例 powerpagecomponent ID 和一个站点语言 ID(从下文步骤 5.2 中的初步发现查询获取——如果尚未获取,请先运行这些查询)运行 discover-component-types.js:
node "${PLUGIN_ROOT}/scripts/lib/discover-component-types.js" \
--envUrl "{envUrl}" \
--token "{token}" \
--websiteRecordId "{websiteRecordId}" \
--powerpageComponentId "{anyPowerpageComponentId}" \
--siteLanguageId "{siteLanguageId}"
将输出作为 JSON 捕获;提取 .websiteComponentType、.subComponentType 和 .siteLanguageComponentType。务必原样使用辅助脚本返回的 JSON 值,不要替换为文档中的“典型”值。 不同租户中观察到的参考值包括 10426/10427/10428 和 10429/10428/10430,但实际值因环境而异,必须通过此脚本的运行时查询获取。三个同级统一实体各自有自己的 componenttype;网站上的 AddRequiredComponents: true 不会包含站点语言,必须显式添加。有关完整的 3 实体模型,请参阅 references/solution-api-patterns.md。
如果脚本报告网站记录尚未加入任何解决方案,请停止并告知用户,必须先部署站点(通过 /power-pages:deploy-site),之后才能将其纳入解决方案。如果不存在 subComponentType(尚未为任何子组件编制索引),则继续操作——你将在步骤 5.2 中发现所有组件 ID。
步骤 5.2 — 发现所有组件
并行运行六个发现查询:
A. 组件类型标签(用于显示名称):
GET {envUrl}/api/data/v9.2/GlobalOptionSetDefinitions(Name='powerpagecomponenttype')
构建 typeLabel 映射:{ [Value]: Label.UserLocalizedLabel.Label }。如果失败,则使用 ${PLUGIN_ROOT}/references/solution-api-patterns.md 第 3b 节中的静态表。
B. 此站点的所有 Power Pages 子组件:
GET {envUrl}/api/data/v9.2/powerpagecomponents
?$filter=_powerpagesiteid_value eq '{websiteRecordId}'
&$select=powerpagecomponentid,name,powerpagecomponenttype
&$orderby=powerpagecomponenttype
遵循 @odata.nextLink 分页。使用 typeLabel 按 powerpagecomponenttype 分组,以显示名称作为分组标签。
C. 站点语言记录:
GET {envUrl}/api/data/v9.2/powerpagesitelanguages?$filter=_powerpagesiteid_value eq '{websiteRecordId}'&$select=powerpagesitelanguageid,languagecode,displayname
保存所有语言 ID。
D. Dataverse 表 — 发现站点实际引用的表,而不是所有与发布者前缀匹配的表。
为什么不按发布者前缀筛选: 使用共享/默认发布者(
new_,环境默认值)时,按前缀匹配会严重过度计数——一个包含 6 个表的站点可能匹配到 22 个无关表——而且当站点的实际表来自不同前缀时(例如由edm发布者创建的bp_*模板),也会漏掉这些表。经 SME 确认,权威信号是站点的表权限:“如果站点使用某个表,就会有相应的权限。” 运行共享发现辅助程序,该程序会将自定义表限定为站点表权限(+ 数据模型清单)与环境中的自定义非托管表的交集:
node "${PLUGIN_ROOT}/scripts/lib/discover-site-components.js" \
--envUrl "{envUrl}" --token "{token}" --siteId "{websiteRecordId}" \
--projectRoot "." \
{if .datamodel-manifest.json elsewhere: --datamodelManifest "<path>"}
使用返回的 customTables[](每项为 { id, logicalName, schemaName, displayName }——id 是 AddSolutionComponent 调用所需的 MetadataId)。这是已经按站点限定的正确列表——不要再按前缀筛选。
关于表的重要说明:Dataverse 解决方案只包含架构——实体定义、列、关系、表单和视图。表的数据/记录不会随解决方案一起传输。如果目标环境需要种子数据/参考数据,则需要单独执行数据迁移。
E. Cloud Flow 链接组件(powerpagecomponenttype 33)——运行时字段自省:
查询将此站点链接到 Cloud Flow 的 powerpagecomponent 记录:
GET {envUrl}/api/data/v9.2/powerpagecomponents
?$filter=_powerpagesiteid_value eq '{websiteRecordId}' and powerpagecomponenttype eq 33
&$select=powerpagecomponentid,name
如果返回了结果,请在不使用 $select 的情况下获取第一条记录,以发现工作流查找字段:
GET {envUrl}/api/data/v9.2/powerpagecomponents({firstComponentId})
扫描响应 JSON,查找值为非空 GUID 且不等于 websiteRecordId 的 _*_value 键。剩余的键就是工作流查找字段(例如 _adx_workflow_value)。使用该字段放入 $select,重新查询所有类型为 33 的组件,以收集所有关联的 workflowId GUID。然后解析每个工作流的名称和状态:
GET {envUrl}/api/data/v9.2/workflows({workflowId})?$select=name,workflowid,statecode
同时发现工作流的组件类型(用于 AddSolutionComponent):
GET {envUrl}/api/data/v9.2/solutioncomponents?$filter=objectid eq '{workflowId}'&$select=componenttype&$top=1
将其存储为 workflowComponentType。如果查询结果为空(该 flow 尚未添加到任何解决方案中),请记录此情况——关联的 flow 记录仍然存在,可以添加。
如果类型 33 的查询未返回记录,请存储 cloudFlows = [] 并跳过。
F. Bot Consumer 链接组件(powerpagecomponenttype 27)——运行时字段自省:
采用与查询 E 相同的模式。查询类型为 27 的 powerpagecomponent 记录,通过对第一条记录进行自省来发现 bot 查找字段,收集 bot GUID,并通过以下方式解析 bot 名称:
GET {envUrl}/api/data/v9.2/bots({botId})?$select=name,botid,statecode
并通过 solutioncomponents 发现 bot 组件类型。将其存储为 botComponents。如果不存在类型为 27 的记录,请存储 botComponents = [] 并跳过。
G. 此解决方案中 Cloud Flow 使用的连接引用:
Cloud Flow 通过 connectionreference 记录引用连接器。这些记录是独立的 Dataverse 实体;如果它们不在解决方案中,解决方案可以顺利导出,但在目标环境中导入会失败,并出现 MissingDependency / 连接引用验证错误。我们必须在此处枚举这些记录,并在步骤 5.6 中添加它们。
如果查询 E 返回 cloudFlows = [],则跳过此查询。
-
查询此站点发布者拥有的连接引用:
GET {envUrl}/api/data/v9.2/connectionreferences ?$filter=startswith(connectionreferencelogicalname,'{publisherPrefix}_') &$select=connectionreferenceid,connectionreferencelogicalname,connectionreferencedisplayname,connectorid -
对于每个 Cloud Flow(来自查询 E),解析其
clientdataJSON(workflows({workflowId})?$select=clientdata),找出它使用的connectionReferenceLogicalName。筛选查询 G.1 的结果,仅保留这些引用——它们是必须加入解决方案的引用。 -
在运行时解析连接引用的 componenttype——该值因环境而异(不同租户中观察到的值包括
10137和10160;不要硬编码):node "${PLUGIN_ROOT}/scripts/lib/discover-component-types.js" \ --envUrl "{envUrl}" --token "{token}" \ --websiteRecordId "{websiteRecordId}" \ --objectIds "{firstConnectionReferenceId}"读取
.resolved[0].componentType并将其存储为connectionReferenceComponentType。如果连接引用尚未加入任何解决方案(resolved[0].componentType === null),说明它从未被添加过——请改为每次调用传入一个 ID,直到解析出结果,或查询一个已知的同类连接引用。若没有运行时解析出的值,不要猜测。将筛选后的列表存储为
connectionReferences[]。
如果查询 G 未返回引用(Cloud Flow 不使用连接器,或发布者前缀不匹配——较少见),请存储 connectionReferences = [] 并跳过。如果存在 Cloud Flow,但未找到匹配的连接引用,请显示软警告——用户应确认其 flow 是否使用了需要在目标环境中绑定的连接器。
步骤 5.3 — 对站点设置进行分类
如果 preloadedSettings 可用(用户在阶段 1 步骤 5 中选择了“使用计划中的预加载选项”),则跳过以下分类——直接使用 preloadedSettings.keepAsIs、preloadedSettings.promoteToEnvVar、preloadedSettings.authNoValue 和 preloadedSettings.credentialNeedsDecision。(2026-05-08 之前生成的计划使用较旧的 excluded 类别;为保持向后兼容,请将其中的内容视为 credentialNeedsDecision。)
否则,运行共享分类器——${PLUGIN_ROOT}/scripts/lib/classify-site-settings.js——它是与 plan-alm 阶段 1 步骤 7 共用的凭据正则表达式和层级映射的唯一真实来源。可以调用 CLI(将 JSON 通过管道传入标准输入),也可以在代码中内联 require()。输出与 plan-alm 生成的四类别结构相同:
{
keepAsIs: [{name}], // Tier 3 — added to the solution unchanged
authNoValue: [{name}], // Tier 2b — Authentication/AzureAD with empty value; added as-is, user sets per-env
promoteToEnvVar: [{name, value}], // Tier 2a — Authentication/AzureAD with value; reviewed at Step 5.4.A
credentialNeedsDecision: [{name, value}] // Tier 1 — credential-style names; bulk-with-override prompt at Step 5.4.C
}
层级定义(与 classify-site-settings.js 中的正则表达式一致):
| 层级 | 类别 | 匹配规则 | 处理方式 |
|---|---|---|---|
| 1 — 凭据类 | credentialNeedsDecision | CREDENTIAL_REGEX(ConsumerKey|ConsumerSecret|ClientId|ClientSecret|AppSecret|AppKey|ApiKey|Password,不区分大小写) | 在步骤 5.4.C 显示带覆盖选项的批量提示 — 自动分类(默认 Secret/String)、全部设为 Secret、全部设为 String、全部跳过,或逐项选择凭据 |
| 2a — 含值的身份验证配置 | promoteToEnvVar | AUTH_PREFIX_REGEX(Authentication/ 或 AzureAD/)且不是凭据,并且有值 | 在步骤 5.4.A 显示多选提示 — 选择哪些设置由环境变量提供 |
| 2b — 无值的身份验证配置 | authNoValue | 相同前缀,无值 | 原样添加到解决方案,并附注说明(用户需按环境设置值) |
| 3 — 其他所有设置 | keepAsIs | 其他任何设置 | 原样纳入解决方案 |
请勿在此处内联正则表达式——如果此技能中的正则表达式有误而 plan-alm 中的正确,计划阶段与执行阶段的分类就会不一致。正则表达式仅存在于 classify-site-settings.js 中;两个技能都必须使用它。
关于 authNoValue 设置的说明:这些是开发环境中尚未设置值的身份验证配置设置。它们将原样添加到解决方案中。部署到每个目标环境后,应在该环境中配置正确的值。在清单审核期间(步骤 5.5),请在警告提示框中列出这些设置。
步骤 5.4 — 处理身份验证设置:提升为环境变量?
呈现最终清单之前,先处理三个非 keepAsIs 类别:
A. promoteToEnvVar 设置(含值的身份验证配置):
🚦 关卡(计划 · setup-solution:5.4a.promote): 对身份验证设置进行多选——选择哪些设置提升为环境变量。其余设置保留为普通站点设置。
通过 AskUserQuestion 并设置 multiSelect: true 提问,列出每项 promoteToEnvVar 设置的名称和当前值:
“这些身份验证配置设置在开发环境中已设置值。如果其中任何设置应在不同环境中使用不同的值(例如,功能标志、登录模式、AzureAD 租户设置),请将其提升为环境变量——这些设置将被跟踪在解决方案中,并在部署时按阶段注入。其余设置保留为普通站点设置。”
- 每项设置对应一个选项(例如
Authentication/Registration/LocalLoginEnabled = true) - 另提供以下选项:“全部提升为环境变量”和“全部保留为普通站点设置”
对于用户选择提升的每项设置:
- 使用
${PLUGIN_ROOT}/scripts/lib/generate-env-var-schema-name.js生成规范架构名称,以确保其符合configure-env-variables和deploy-pipeline后续所需的名称:
输出:node "${PLUGIN_ROOT}/scripts/lib/generate-env-var-schema-name.js" \ --publisherPrefix "{prefix}" \ --settingName "{settingName}"{ schemaName, sanitized }。此帮助程序是规范规则({prefix}_{sanitized(settingName)}.toLowerCase())的唯一真实来源——请勿将规则内联。对于相同的逻辑设置,setup-solution 和 configure-env-variables 必须生成相同的架构名称;内联规则可能导致输出不一致。 - 使用解析出的架构名称创建
environmentvariabledefinition:
对于身份验证配置设置,请使用类型node "${PLUGIN_ROOT}/scripts/lib/create-env-var-definition.js" \ --envUrl "{envUrl}" \ --token "{token}" \ --schemaName "{schemaName from step 1}" \ --displayName "{friendlyName}" \ --type 100000000100000000(String),而不是 Secret——这些是功能标志,不是凭据。将输出捕获为 JSON;提取.definitionId和.schemaName。 - 记录
definitionId,以便将其添加到组件列表中(步骤 5.6,ComponentType: 380)。 - 将站点设置链接到环境变量,使用
link-site-setting-to-env-var.js:
检查node "${PLUGIN_ROOT}/scripts/lib/link-site-setting-to-env-var.js" \ --envUrl "{envUrl}" \ --token "{token}" \ --siteSettingId "{settingId}" \ --definitionId "{definitionId}" \ --schemaName "{schemaName}".ok和.verified是否均为true。
用户选择不提升的设置会从 promoteToEnvVar 移至 keepAsIs——它们将作为普通站点设置包含在解决方案中。
B. authNoValue 设置(身份验证配置,无开发环境值):
无需用户决定。这些设置会自动按原样包含在解决方案中。在步骤 5.5 中,将它们显示在警告框中:
“以下身份验证设置在开发环境中尚未设置值。它们将按原样添加到解决方案中。部署到各个目标环境后,请确认或设置相应的值。”
C. credentialNeedsDecision 设置(凭据类型——批量并可覆盖的提示):
这些是凭据类型的站点设置(ConsumerKey / ClientSecret 等),在加入解决方案前需要做出决定。将原始值打包在解决方案 zip 中会造成实际泄露风险,因此安全做法是将站点设置记录添加到解决方案中,并通过环境变量按阶段传递值。当 N 较大时,逐个询问每项凭据过于繁琐(一个典型站点可能会有 20 多项来自多个 OAuth 提供商、与身份验证相关的凭据),因此此步骤使用批量并可覆盖的提示:一个问题涵盖全部 N 项凭据,同时允许针对单项凭据进行单独调整。
步骤 5.4.C.1 — 按名称模式自动分类。
对每项设置调用 ${PLUGIN_ROOT}/scripts/lib/classify-site-settings.js 中的 autoClassifyCredential(name)。该辅助函数按顺序应用以下正则表达式(唯一事实来源——请勿在此处重复定义):
| 默认值 | 辅助函数中的匹配器 | 触发条件 |
|---|---|---|
Secret 环境变量(type: 100000005) | CREDENTIAL_SECRET_REGEX(Secret|Password|ApiKey|AppKey) | 名称中包含这些子串——*ClientSecret、*AppSecret、*Password、*ApiKey、*AppKey |
String 环境变量(type: 100000000) | CREDENTIAL_STRING_REGEX(Id|ConsumerKey),且不匹配 Secret | 类似 *ClientId、*ConsumerKey、*TenantId、*AppId 的名称 |
| Secret 环境变量(回退) | (辅助函数在两者都不匹配时采用的防御性默认值) | 其他所有情况——出于谨慎,默认将凭据名称视为敏感信息 |
对于每项设置,辅助函数返回 { default: 'secret' | 'string', reason }。将结果分组为 AUTO_CLASSIFY = { secrets: [...], strings: [...] },并向用户显示一行摘要:“已自动分类 {N} 项凭据类型设置:{S} 项为 Secret 环境变量(每个阶段使用 Key Vault),{T} 项为 String 环境变量(每个阶段使用纯文本)。”
步骤 5.4.C.2 — 批量提示。
🚦 关卡(同意 · setup-solution:5.4c.credentials): 批量凭据处理决策——Secret 环境变量(每个阶段使用 Key Vault)、String 环境变量(每个阶段使用纯文本),或跳过。可针对单项凭据单独选择。此决策会影响 Secret 值是否会打包在解决方案 zip 中。
提出一个涵盖全部 N 项凭据的 AskUserQuestion:
“检测到 {N} 项凭据类型站点设置(
{firstFew.join(', ')}{N>3?', ...':''})。应如何处理?将这些值打包在解决方案 zip 中会造成实际泄露风险,因此推荐的做法是将站点设置记录添加到解决方案中,并通过环境变量按阶段传递值。实际的 Secret 值不会打包在 zip 中——而是在每个环境中通过
deploymentsettingsjson设置。” 选项:
- 按名称自动分类 (推荐) — 应用第 5.4.C.1 步中的自动分类:{S} 作为 Secret 环境变量,{T} 作为 String 环境变量。只需确认一次,即可处理全部 {N} 项。(默认选项。)
- 全部作为 Secret 环境变量 — 将每项凭据都视为由 Key Vault 支持的 Secret 环境变量。此选项较为保守;适用于任何凭据,但会为实际上不需要的阶段值增加 Key Vault 依赖。
- 全部作为 String 环境变量 — 将每项凭据都视为纯文本的分阶段环境变量。仅当所有凭据都不是真正的机密(例如仅供内部使用的测试设置)时使用。
- 全部跳过 — 不将任何项添加到解决方案中。用户按环境在解决方案之外管理所有凭据值。等同于 IronItOut 之前的“excluded”行为。
- 逐项选择 — 对每项凭据分别提示,以便进行精细控制(每项设置可选 Secret / String / Skip)。当真正的机密与不敏感 ID 混杂,且无法通过自动分类准确处理时,可选择此项。
分支逻辑:
- 选项 1(自动分类):对
AUTO_CLASSIFY.secrets中的每项设置,按以下步骤创建环境变量,并使用--type 100000005。对AUTO_CLASSIFY.strings中的每项设置,使用--type 100000000。无需额外提示。 - 选项 2(全部设为 Secret):将全部 N 项视为 Secret。使用
--type 100000005执行相同循环。 - 选项 3(全部设为 String):将全部 N 项视为 String。使用
--type 100000000执行相同循环。 - 选项 4(全部跳过):将全部 N 项移入
userOptedOutOfSolution分类。在第 5.5 步显示:"以下凭据类设置应用户要求已跳过,且不在解决方案中。请在部署后于每个目标环境中手动配置:{names}。" - 选项 5(逐项选择):对每项设置运行一个包含 3 个选项的
AskUserQuestion(Secret 环境变量 / String 环境变量 / Skip)。自动分类结果用于确定每次提示的默认选项,但用户可以覆盖。
第 5.4.C.3 步 — 创建环境变量(适用于选项 1、2、3,以及选项 5 中选择非 Skip 的项)。
对于每项选择使用环境变量处理的设置:
-
使用
${PLUGIN_ROOT}/scripts/lib/generate-env-var-schema-name.js生成规范架构名称(与第 5.4.A 步使用相同的辅助程序——以此作为唯一可信来源,确保 configure-env-variables 和 deploy-pipeline 之后可以引用相同的架构名称):node "${PLUGIN_ROOT}/scripts/lib/generate-env-var-schema-name.js" \ --publisherPrefix "{prefix}" \ --settingName "{settingName}" -
使用解析出的架构名称创建
environmentvariabledefinition:node "${PLUGIN_ROOT}/scripts/lib/create-env-var-definition.js" \ --envUrl "{envUrl}" \ --token "{token}" \ --schemaName "{schemaName from step 1}" \ --displayName "{friendlyName}" \ --type "{100000005 for Secret, 100000000 for String}"对于 Secret 环境变量,不要传入
--defaultValue——开发值应按阶段存入 Key Vault,而不是存入定义中。对于 String 环境变量,将开发值设为默认值。 -
记录
definitionId,以便将其纳入组件列表(第 5.6 步,ComponentType: 380)。 -
使用
link-site-setting-to-env-var.js将站点设置关联到环境变量(调用方式与上面的第 5.4.A 步相同)。 -
站点设置本身也会与环境变量定义一起添加到解决方案中——两者都是受跟踪的组件。
如果创建任何一个环境变量失败(例如循环中令牌过期、架构名称冲突等),请显示失败的设置名称和原因,并询问用户是重试、仅跳过该设置,还是中止整个批量操作。不得静默丢弃凭据。
向后兼容性:读取 2026-05-08 之前生成的 preloadedSettings 计划时,将 preloadedSettings.excluded 中的所有条目视为 credentialNeedsDecision,并运行上述带覆盖选项的批量提示。
第 5.4b 步 — 采用孤立的环境变量定义
除了上述 OAuth 密钥转换外,其他技能(尤其是 setup-auth、add-server-logic 和 configure-env-variables)之前可能创建过环境变量定义,但从未将其添加到用户解决方案中——它们会进入 Default 解决方案并悄然偏离。此步骤会发现并采用这些定义。
运行共享发现辅助程序,一次性获取完整的网站清单:
node "${PLUGIN_ROOT}/scripts/lib/discover-site-components.js" \
--envUrl "{envUrl}" --token "{token}" \
--siteId "{websiteRecordId}" \
--publisherPrefix "{publisherPrefix}" \
--solutionId "{solutionId}"
将标准输出解析为 JSON,并读取 missing.envVars——其中包含 schemaname 以发布者前缀开头、但尚未作为此解决方案的 solutioncomponents 的环境变量定义。
对于每个条目,还要查询它当前所属的解决方案(这样用户就能区分仅属于 Default 的孤立项,以及由其他用户解决方案有意管理的环境变量):
GET {envUrl}/api/data/v9.2/solutioncomponents
?$filter=objectid eq {definitionId}&$select=_solutionid_value
然后获取每个匹配项所属解决方案的 uniquename。为每个环境变量添加标签:
DEFAULT-ONLY— 仅由Default解决方案管理(其他技能产生的典型孤立项)。IN OTHER SOLUTION: <uniquename>— 由用户解决方案管理;用户可能有意希望将其限定在该解决方案中。
如果至少有一个环境变量带有 DEFAULT-ONLY 标签,则通过 AskUserQuestion 提示,设置 multiSelect: true:
🚦 关卡(计划 · setup-solution:5.4b.orphan-envvars): 采用符合发布者前缀、但尚未纳入此解决方案的环境变量定义。
DEFAULT-ONLY孤立项会预选为推荐项;已由其他用户解决方案管理的环境变量会列出,但不会预选(只有用户打算转移所有权时才选择)。
"我们发现了一些带有发布者前缀 (
{prefix}_)、但尚未纳入 {solutionUniqueName} 的环境变量定义。请选择要纳入的定义。仅包含定义——值仍按环境分别保存,不会随之迁移。
{schemaName}({displayName})— 类型 {type},当前所属:{tag}- ...
另外:包含所有仅属于 Default 的孤立项(推荐) / 暂时跳过"
将选中的条目收集到 adoptedEnvVars: [{ definitionId, schemaName, displayName, type }]。
如果没有选中任何条目或列表为空,adoptedEnvVars 保持为空——技能将静默继续。
此步骤存在的原因:在此检查之前,其他技能创建的环境变量会被静默排除在网站解决方案之外,也不会迁移到暂存环境或生产环境。在此处展示这些变量,是遵循
AGENTS.md中“默认考虑 ALM”原则所要求的跨技能安全网。
第 5.4c 步 — 采用孤立的 Power Pages 组件
与第 5.4b 步类似,但针对 powerpagecomponent 行。此步骤会捕获网站上由其他技能或 pac pages upload-code-site 创建、但未纳入用户解决方案的组件。2026-04-22 实时验证中发现的典型示例:
invoice-checker服务器逻辑(类型 35)——在之前的会话中通过/power-pages:add-server-logic添加,从未注册到用户解决方案中。index.html(类型 3)——由pac pages upload-code-site刷新、当前 SPA 的入口页面;每次重新构建都会创建新的index.html记录,但不会自动将其添加到用户解决方案中。
使用共享发现辅助程序收集孤立项列表(它已排除 Vite/Rollup 捆绑包分块——Home-XYZ.js、index-XYZ.css 等——因此提示不会被带哈希的无关项淹没):
node "${PLUGIN_ROOT}/scripts/lib/discover-site-components.js" \
--envUrl "{envUrl}" --token "{token}" \
--siteId "{websiteRecordId}" \
--publisherPrefix "{publisherPrefix}" \
--solutionId "{solutionId}"
从 JSON 输出中读取 missing.powerpagecomponents 并进行分类:
- 真实内容孤立项 —
name不匹配捆绑包分块正则表达式([-.][A-Za-z0-9_-]{7,14}\.(js|mjs|cjs|css)(\.map)?$)的条目。这些条目应予以采用。 - 捆绑包分块孤立项 — 汇总时计数,但不要提示用户采用。它们是过时的构建产物,不是真实内容。在第 7 阶段的摘要中报告这些条目,并建议通过单独的维护流程进行清理。
对于每个真实内容孤儿,还要按
name去重:如果有多条index.html记录,且其中一条已在解决方案中(modifiedon更新),那么较旧的孤儿就是过时的重复项——不要将其加入采纳提示词,而应将其记录为过时重复项。规则:每个(powerpagecomponenttype, name)对仅保留最新的孤儿。
还要处理 missing.siteLanguages——这些是 powerpagesitelanguage 记录(组件类型 10428),存在于站点上,但不在用户的解决方案中。它们不是可选项:导入的站点如果缺少语言记录,身份验证后将无法正常渲染,因为 powerpagesite.content.defaultlanguage 引用了目标环境中不存在的 ID。请将每个条目原样纳入孤儿采纳提示词——语言没有捆绑包块噪声需要过滤——并预先选中为推荐项。
如果真实内容孤儿列表(或 missing.siteLanguages)非空,则通过 AskUserQuestion 提示,设置 multiSelect: true:
🚦 关卡(plan · setup-solution:5.4c.orphan-ppcs): 采纳存在于站点上、但不在此解决方案中的孤儿
powerpagecomponent行(包括powerpagesitelanguage记录)。站点语言预先选中为推荐项,因为省略它们会悄无声息地导致身份验证后渲染失败。其他 ppc 孤儿(例如invoice-checker服务器逻辑)如果看起来是真实内容,则预先选中;过时的构建产物捆绑包块已在上游过滤。
"发现 {N} 个尚未纳入 {solutionUniqueName} 的站点组件:
{name}(类型 {type} {typeLabel})— 当前所在位置:{currentSolution}- ...
另外:纳入所有孤儿(推荐) / 暂时跳过"
将用户的选择收集到 adoptedPpcs: [{ id, name, type, typeLabel }]。
用户进行选择后,对每个条目调用 AddSolutionComponent,并设置 AddRequiredComponents: false 和正确的 ComponentType(使用 Step 5.1 中由 discover-component-types.js 解析出的值——不要硬编码):
- 对于
missing.powerpagecomponents条目,使用subComponentType - 对于
missing.siteLanguages条目,使用siteLanguageComponentType
不要设置 DoNotIncludeSubcomponents: true——对于非 Entity 根组件,Dataverse API 会拒绝该标志(HTTP 400 0x80040216),而且这些统一实体行也不需要此标志。
如果 missing.powerpagecomponents(过滤后)和 missing.siteLanguages 均为空,则此步骤静默运行。
此步骤的存在原因:在加入此项检查之前,反复出现的问题是,
setup-solution完成后,用户以为一切都已处理妥当,但invoice-checker/index.html/ 类似的站点关联记录却悄悄留在Active解决方案中,没有被部署到预发布环境或生产环境。今天的实时验证在 SupplierInvoicePortal 上发现了 1 个真实孤儿(invoice-checker)——通过 AddSolutionComponent 采纳后,解决方案版本从 v1.0.0.1 升至 v1.0.0.2。
Step 5.5 — 展示完整清单并征求用户确认
这是关键决策点。 构建一份将要添加的所有内容的完整清单,并在写入任何内容之前展示给用户。
如果发现了自定义表(步骤 D 中网站引用的集合),请先通过 AskUserQuestion 并设置 multiSelect: true 征求用户选择,然后再展示最终清单:
- 第一个选项:“包含所有 N 个被引用的表(推荐)” — 默认预选。N 是网站实际引用的表的数量(而非整个环境中的前缀列表)。
- 然后为每个表提供一个选项:
{logicalName} ({DisplayName}) - 最后一个选项:“排除所有表”
默认列表已限定为网站实际使用的表(步骤 D)。如果用户知道网站还需要某个尚未获得权限的其他表,可以手动添加;但默认列表绝不能是发布者前缀下所有表的倾倒。
以结构化摘要的形式展示:
Here is everything that will be added to solution "{solutionName}":
WEBSITE & LANGUAGE
✓ Website record: {siteName}
✓ Site language(s): English (en-US)
SITE COMPONENTS ({total} components across {K} types)
✓ Publishing States (2)
✓ Web Pages (10)
✓ Web Files (90) — compiled JS/CSS/HTML assets
✓ Page Templates (5)
✓ Web Templates (13)
✓ Content Snippets (11)
✓ Web Roles (2)
✓ Website Access (6)
✓ Table Permissions (13) — required for Web API authorization in target env
✓ Site Markers (5)
✓ Webpage Rules (2)
SITE SETTINGS (64 included)
✓ Web API settings (14): Webapi/crd50_invoice/enabled, ...
✓ Feature flags (32): CodeSite/Enabled, Search/Enabled, ...
✓ Auth config (18): Authentication/Registration/LocalLoginEnabled, ...
~ OAuth as env vars (3): ids_auth_openauth_microsoft_clientsecret, ... [ENV VAR]
✗ OAuth excluded (5): Authentication/OpenAuth/Facebook/AppSecret, ... [EXCLUDED]
CLOUD FLOWS ({N} linked via powerpagecomponent type 33)
✓ Invoice Approval Flow (workflowId: {guid}, Active)
~ Draft Flow (workflowId: {guid}, Inactive — excluded by default)
BOT CONSUMERS ({N} linked via powerpagecomponent type 27)
✓ Support Bot (botId: {guid}, Active)
DATAVERSE TABLES (schema only — no data)
✓ crd50_invoice (Invoice)
...
ENV VAR DEFINITIONS (componenttype 380)
✓ ids_auth_openauth_microsoft_clientsecret (Secret) [converted from OAuth secret]
✓ crd50_auth_openauth_microsoft_clientsecret (Secret) [ADOPTED — was in Default only]
...
Total to add: ~{N} components
为清晰起见,在清单中的每个环境变量条目后使用以下标签:
[由 OAuth 密钥转换而来]— 在步骤 5.4 中根据网站设置创建[已采用 — 之前仅在 Default 中]— 在本次运行前已存在;在步骤 5.4b 中被加入解决方案[已采用 ppc — 之前仅在 Active 中]— 在步骤 5.4c 中采用了 powerpagecomponent(例如invoice-checker服务端逻辑;实际网站页面尚未注册)[已采用 — 也在 {otherSolutionName} 中]— 已存在于另一个用户解决方案中;正在额外添加到此处(用户已明确选择加入)
如果 cloudFlows 不为空,请通过 AskUserQuestion 并设置 multiSelect: true 征求用户选择:
- 选项:“包含所有 N 个活动云流(推荐)”
- 为每个流提供一个选项:
{name} ({workflowId}) - 选项:“排除所有云流”
默认:包含活动流,排除非活动流。如果某个流已属于其他解决方案,请警告用户:“此流位于解决方案 X 中 — 将其添加到此处会移动它。”
如果 botComponents 不为空,请通过 AskUserQuestion 并设置 multiSelect: true 征求用户选择(采用相同模式)。
如果两者都为空,则跳过并显示 (未发现)
展示清单摘要后,提供一个自由文本补充入口:
“如果你知道还有哪些应包含在此解决方案中、但未在上方列出的云流或机器人,请在此处粘贴其 GUID(以逗号分隔)。留空以继续。”
🚦 关卡(plan · setup-solution:5.5.manifest-confirm): 在任何
AddSolutionComponent写入操作之前确认最终清单。涵盖表、流、机器人、环境变量及孤立组件的采用。在此处取消会保留内存中的清单,但不会对 Dataverse 执行任何写入。
通过 AskUserQuestion 征求用户确认:
“这样看起来对吗?你可以继续,也可以告诉我需要排除哪些类别或表。”
选项:“按此选择继续” / “我想进行更改”
等待用户明确确认后再执行步骤 5.6。
Step 5.6 — 添加所有已确认的组件
构建一个包含所有待添加组件的 JSON 数组,然后调用 scripts/lib/add-components-to-solution.js 执行批量操作;其中内置了令牌刷新和幂等性处理。
组件数组应按以下顺序构建:
- 网站记录——
{ componentId: websiteRecordId, componentType: websiteComponentType, addRequired: true, description: "Website: {siteName}" } - 站点语言记录——每种语言一条记录,使用
siteLanguageComponentType(不会由AddRequiredComponents自动包含) - 所有已确认的 powerpagecomponent 组——每个组件一条记录,使用
subComponentType- 表权限(类型 18)属于标准 powerpagecomponents,默认包含
- 排除未转换为环境变量的 OAuth 密钥站点设置
- 环境变量定义——每条定义一条记录,使用
{ componentType: 380 }。包括:- 在步骤 5.4(OAuth 密钥转换)中创建的所有环境变量
- 步骤 5.4b 中
adoptedEnvVars的每一项(用户选择包含的孤立项)
- Dataverse 表——
{ componentType: 1, componentId: MetadataId } - 已确认的云端流(来自步骤 5.5)——
{ componentId: workflowId, componentType: workflowComponentType }(使用运行时发现的类型) - 已确认的机器人组件——
{ componentId: botId, componentType: botComponentType }(使用运行时发现的类型) - 已确认的云端流使用的连接引用(来自步骤 5.2 查询 G)——每个引用一条记录:
{ componentId: connectionReferenceId, componentType: connectionReferenceComponentType, addRequired: false }。如果connectionReferences = [],则跳过。使用运行时解析的connectionReferenceComponentType——不要硬编码(不同租户中观察到的值包括10137和10160;该值因环境而异)。缺少这些记录时,解决方案可以顺利导出,但目标导入会因MissingDependency错误而失败——deploy-pipeline阶段 6.6.1 会将其显示为“缺少连接引用”验证失败。 - 已采纳的孤立 ppc(来自步骤 5.4c)——
{ componentId: ppc.id, componentType: subComponentType, addRequired: false }。使用步骤 5.1 中discover-component-types.js解析出的subComponentType值——不要硬编码。不要设置DoNotIncludeSubcomponents: true——Dataverse 会拒绝在非 Entity 组件上使用此标志(HTTP 4000x80040216)。
单解决方案模式(MULTI_SOLUTION_MODE = false):将数组写入临时文件(例如,C:/Users/{user}/AppData/Local/Temp/components-to-add.json),然后运行:
node "${PLUGIN_ROOT}/scripts/lib/add-components-to-solution.js" \
--envUrl "{envUrl}" \
--componentsFile "C:/Users/{user}/AppData/Local/Temp/components-to-add.json" \
--solutionUniqueName "{solutionUniqueName}"
多解决方案模式(MULTI_SOLUTION_MODE = true):根据各解决方案的 componentTypes(策略 3 还包括 tableLogicalNames),将统一组件列表划分到 PROPOSED_SOLUTIONS 中,然后针对每个解决方案运行一次 add-components-to-solution.js。每个解决方案的循环 SHOULD 按顺序串行运行(每次 helper 调用已经会在内部批处理并刷新令牌;并行运行多个解决方案会增加令牌刷新负载,而不会真正缩短总耗时,因为瓶颈在于每个批次内部逐个调用 Dataverse)。对于 PROPOSED_SOLUTIONS 中的每个条目(跳过 isFutureBuffer: true):
- 将统一组件数组筛选为 Dataverse 类型名称映射到该解决方案
componentTypes数组中的组件。数值型componentType→ 名称的映射,与discover-component-types.js和discover-site-components.js使用的映射相同(PPC_TYPE_LABELS)。表将路由到tableLogicalNames包含该表逻辑名称的解决方案(策略 3),或路由到声明了'Table'的解决方案(策略 1 和 2)。 - 将每个解决方案对应的子数组写入临时文件(例如
C:/Users/{user}/AppData/Local/Temp/components-{uniqueName}.json),然后使用全部三个必需标志调用 helper:
捕获以node "${PLUGIN_ROOT}/scripts/lib/add-components-to-solution.js" \ --envUrl "{envUrl}" \ --componentsFile "C:/Users/{user}/AppData/Local/Temp/components-{uniqueName}.json" \ --solutionUniqueName "{proposedSolutions[i].uniqueName}"uniqueName为键的 JSON 摘要,并删除临时文件。三个标志都必不可少——如果省略--envUrl或--componentsFile(只传入--solutionUniqueName),helper 将以退出码 1 退出,并提示--envUrl is required/--componentsFile is required。每次迭代都必须传入这两个参数,即使循环中所有迭代的--envUrl都相同。 - 如果某个组件的类型与任何解决方案的
componentTypes都不匹配,则逐个组件发出警告并停止——划分过程丢失了组件。这通常意味着拆分计划遗漏了某种类型(compute-split-plan.js中出现回归);用户需要重新规划,而不是让组件悄悄泄漏到Default中。
如果 helper 通过名称解析不够充分,则使用 Phase 4 中的 SOLUTIONS_BY_NAME 将每个 uniqueName 解析为对应的 solutionId。
该脚本每 20 次调用刷新一次令牌,将“已在解决方案中”视为成功,并输出 JSON 摘要 { total, success, skipped, failed, failures }。完成后删除临时文件。
阶段 6 — 验证并编写清单
- 验证组件:
GET {envUrl}/api/data/v9.2/solutioncomponents?$filter=_solutionid_value eq '{solutionId}'&$select=objectid,componenttype - 按类型统计组件,确认网站记录(使用
websiteComponentType)存在
2b. 为渲染后的 ALM 计划捕获设置完成后的环境变量快照。 确保 docs/alm/ 存在,然后运行发现辅助程序,并将其输出写入旁路标记文件(docs/alm/last-env-vars.json)。计划刷新辅助程序(阶段 7 的自刷新)会将此旁路文件载入 planData.envVars,使渲染后的计划中的环境变量选项卡显示 setup-solution 刚创建或采用的定义(否则,即使阶段 5.4 / 5.4.C / 5.4b 已创建定义,该选项卡仍会为空):
node -e "require('fs').mkdirSync('docs/alm',{recursive:true})"
node "${PLUGIN_ROOT}/scripts/lib/discover-env-var-definitions.js" \
--envUrl "{envUrl}" \
--publisherPrefix "{publisherPrefix}" \
--websiteRecordId "{websiteRecordId}" \
--token "{token}" > docs/alm/last-env-vars.json.tmp \
&& mv docs/alm/last-env-vars.json.tmp docs/alm/last-env-vars.json
临时文件写入模式可在临时发现失败时保留之前有效的 docs/alm/last-env-vars.json(与 plan-alm 阶段 1 中的 docs/alm/alm-size-estimate.json 模式相同)。如果辅助程序以非零状态退出,则记录 stderr 并继续——现有旁路文件(或没有旁路文件)均可接受;本次刷新只是不更新环境变量。
旁路文件的结构与 discover-env-var-definitions.js 已返回的结构一致:{ envVars: [{ schemaName, type, defaultValue, siteSetting }], count }。不要转换——渲染器会直接读取这些字段。
-
将
.solution-manifest.json写入项目根目录(与powerpages.config.json并列):- 清单格式参见
${PLUGIN_ROOT}/references/solution-api-patterns.md第 7 节 - 如果已确认存在云端流,则包含
cloudFlows数组:[{ "workflowId": "...", "name": "...", "status": "active|inactive" }] - 如果已确认存在机器人组件,则包含
botComponents数组:[{ "botId": "...", "name": "..." }] - 如果未发现或确认任何流/机器人,则完全省略这些数组(不存在 = 未跟踪;
[]= 已跟踪但未选择任何项)
在
MULTI_SOLUTION_MODE中,编写清单 v2,包含solutions[]数组:{ "schemaVersion": 2, "publisher": { "publisherId": "...", "uniqueName": "...", "friendlyName": "...", "customizationPrefix": "..." }, "solutions": [ { "uniqueName": "IdeaSphere_Core", "solutionId": "...", "version": "1.0.0.0", "order": 1, "componentTypes": ["Table", "Site Setting", ...], "components": [ { "componentId": "...", "componentType": 1, "description": "..." } ] }, { "uniqueName": "IdeaSphere_WebAssets", "solutionId": "...", "version": "1.0.0.0", "order": 2, "componentTypes": ["Web File"], "components": [ ... ] } ], "splitStrategy": "strategy-1-layer", "assetAdvisory": [ /* pass-through from plan context */ ] }v1 单解决方案清单保持向后兼容。 读取方(
export-solution、import-solution、setup-pipeline、deploy-pipeline)会检查schemaVersion:schemaVersion缺失或为1→ 按单解决方案处理(现有行为)。schemaVersion: 2→ 按order遍历solutions[]。
- 清单格式参见
-
提交:
git add .solution-manifest.json && git commit -m "Add solution manifest for ALM"
阶段 7 — 显示摘要
显示摘要表:
| 项目 | 值 |
|---|---|
| 发布者 | {friendlyName}({uniqueName},前缀:{prefix}) |
| 解决方案 | {friendlyName}({uniqueName},v{version}) |
| 解决方案 ID | {solutionId} |
| 已添加的组件 | N |
| 已添加的环境变量定义 | N(如有 OAuth 密钥转换) |
| 清单文件已写入 | .solution-manifest.json |
如果有任何身份验证设置已提升为环境变量,请确认每个站点设置都已自动关联。显示简短确认:
Auth settings promoted to environment variables:
✓ Authentication/Registration/LocalLoginEnabled → ids_authentication_registration_localloginenabled
✓ Authentication/Registration/AzureADLoginEnabled → ids_authentication_registration_azureadloginenabled
注意:仍须通过 configure-env-variables 或 Power Pages 管理 UI 设置各环境的值。
如果包含任何 authNoValue 设置,请显示提醒:
Auth settings included without a dev value (configure in each target env after deploy):
⚠ Authentication/OpenAuth/Facebook/AppId
⚠ Authentication/Registration/LoginButtonAuthenticationType
🚦 关卡(plan · setup-solution:7.next-step): 为后续部署技能选择路由 — PP Pipelines、手动导出/导入,或暂缓。此技能的所有 Dataverse 写入操作均已完成;此关卡用于选择接下来运行的内容。
通过 AskUserQuestion 询问用户接下来想做什么:
"你希望如何将此解决方案部署到其他环境?"
选项:
- “使用 Power Platform Pipelines(推荐)” — 在 PP Pipelines 宿主环境中设置管道;支持分阶段部署、审批关卡和各阶段的环境变量覆盖。
- “手动导出和导入” — 将解决方案导出为 zip 并直接导入目标环境。适用于一次性部署,操作更简单。
- “我稍后再决定” — 显示后续步骤建议并退出。
如果用户选择选项 1,立即调用 /power-pages:setup-pipeline。
如果用户选择选项 2,立即调用 /power-pages:export-solution。
如果用户选择选项 3,显示:
- 运行
/power-pages:setup-pipeline以进行自动化分阶段部署 - 运行
/power-pages:export-solution以导出 zip,供手动导入 - 如果需要为各阶段设置特定于环境的值,运行
/power-pages:configure-env-variables
提示:稍后添加组件
当实时站点的内容超出此解决方案所包含的范围时 — 例如来自
add-server-logic的服务器逻辑、来自add-cloud-flow的云流、来自setup-auth或configure-env-variables的环境变量、来自setup-datamodel的新表或新的 Web 角色 — 请重新运行/power-pages:setup-solution。当项目根目录中存在.solution-manifest.json时,该技能会自动检测同步模式,执行发现流程,将实时站点与解决方案进行比较,提升版本号,并仅添加缺少的组件。无需单独执行“添加到解决方案”工作流。
记录技能使用情况
参考:
${PLUGIN_ROOT}/references/skill-tracking-reference.md
按照参考文档中的技能跟踪说明记录此技能的使用情况。使用 --skillName "SetupSolution"。
刷新 ALM 计划(如果存在)
node "${PLUGIN_ROOT}/scripts/lib/refresh-alm-plan-data.js" \
--projectRoot "." \
--phase setup-solution \
--render
该辅助工具会将 planData.plannedEnvVarCount 重置为 0(计划中的环境变量已创建,或按用户要求跳过),并重新渲染 docs/alm-plan.html,使概览统计卡片和环境变量选项卡反映设置完成后的状态。如果 docs/.alm-plan-data.json 不存在(单独调用,不属于 ALM 计划),该辅助工具会返回 ok:false,作为软性空操作——可以放心地无条件运行。
告知用户下一步(由用户驱动执行顺序)。 辅助工具的标准输出 JSON 包含 nextStep: { name, skill: string | null } | null。如果下一待处理步骤没有可由用户调用的命令(例如内部的“Finalize”步骤),skill 就是 null——因此请根据它进行分支处理:
nextStep.skill非 null → “计划已更新。计划中的下一步:{nextStep.name} → 准备好后运行{nextStep.skill}。”nextStep.skill为null→ 只说出步骤名称,不要附带命令:“计划已更新。计划中的下一步:{nextStep.name}。”绝不要输出run null。
如果 nextStep 本身为 null(所有计划步骤均已完成),或者辅助工具返回 ok:false(磁盘上没有计划),就不要提及下一步。绝不要自动调用下一个技能——plan-alm 是规划工具,用户会一次执行一个技能。
关键决策点(等待用户)
- 阶段 2:确认发布者前缀——这是永久设置,无法更改
- 阶段 3:确认复用还是创建——在进行任何写入之前
- 阶段 1,步骤 5:ALM 计划上下文——使用
plan-alm预加载的网站设置分类,或重新发现并分类 - 阶段 5,步骤 5.4:包含值的身份验证设置——多选哪些设置要提升为环境变量,哪些保留为普通网站设置;对于凭据类型设置,逐项提示(Secret 环境变量 / String 环境变量 / 跳过)
- 阶段 5,步骤 5.5:完整清单审查——用户查看所有内容(网站、网站语言、所有组件类别、表、环境变量定义、
authNoValue警告),并在写入任何组件之前确认或调整 - 阶段 7:下一步——PP Pipelines(推荐)、手动导出/导入,或稍后决定
错误处理
- 如果创建发布者时失败并出现“duplicate”错误:重新查询并使用现有发布者
- 如果创建解决方案时失败并出现“duplicate”错误:重新查询并使用现有解决方案
- 如果
AddSolutionComponent返回“already in solution”:视为成功(幂等) - 失败时绝不尝试回滚——报告成功和失败的项目
进度跟踪表
| 任务主题 | 进行时 | 描述 |
|---|---|---|
| 验证先决条件 | 正在验证先决条件 | 确认 PAC CLI 身份验证,获取 Azure CLI 令牌,验证 API 访问权限,定位 powerpages.config.json |
| 收集解决方案配置 | 正在收集解决方案配置 | 从用户处收集发布者名称、前缀、解决方案名称和版本——确认不可逆的选择 |
| 检查现有发布者和解决方案 | 正在检查现有状态 | 查询 Dataverse 中现有的发布者和解决方案,避免重复创建 |
| 创建发布者和解决方案 | 正在创建发布者和解决方案 | 通过 Dataverse OData API POST 发布者和解决方案,并记录 ID |
| 将网站组件添加到解决方案 | 正在添加网站组件 | 通过运行时字段自省发现网站/语言/powerpagecomponents/表/云流(类型 33)/机器人使用者(类型 27);按类别拆分网站设置;呈现包含 CLOUD FLOWS 和 BOT CONSUMERS 部分及活动/非活动状态的完整清单;获取用户确认;调用 add-components-to-solution.js 添加网站、网站语言、所有已确认组件、表(ComponentType=1)、已确认的云流和已确认的机器人组件 |
| 验证并写入清单 | 正在验证解决方案并写入清单 | 确认解决方案中的组件,写入 .solution-manifest.json,并提交 |
| 显示摘要 | 正在显示摘要 | 显示解决方案详情、组件数量和后续步骤 |