mcpField
S
setup-solution

setup-solution

github.com/microsoft云与 DevOps1.1k164 次安装今天更新
代码仓库

关于 setup-solution

setup-solution 是面向 Power Pages 项目的 ALM 技能,用于在 Dataverse 中创建发布者和解决方案,并将站点组件加入解决方案。它还生成供后续导出、导入和流水线技能使用的清单。

主要功能

  1. 01创建 Dataverse 发布者和解决方案
  2. 02将 Power Pages 组件加入解决方案
  3. 03支持现有解决方案同步
  4. 04生成解决方案清单
  5. 05支持多解决方案计划

适用场景

  1. 01为 Power Pages 项目建立可部署的解决方案
  2. 02将现有解决方案与站点组件清单同步
  3. 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 — 验证前提条件

在此阶段开始时一次性创建所有任务。

要创建的任务:

  1. “验证前提条件”
  2. “收集解决方案配置”
  3. “检查现有发布者和解决方案”
  4. “创建发布者和解决方案”
  5. “将站点组件添加到解决方案”
  6. “验证并写入清单”
  7. “提供摘要”

步骤:

  1. 运行 pac env who — 提取 environmentUrl、organizationId(向用户显示以供确认)

  2. 运行 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)。

  3. 定位 powerpages.config.json — 读取 siteName 和 websiteRecordId

  4. 确认 .powerpages-site/ 文件夹存在(查找组件记录时必需)

  5. 检查 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 并重新分类)。
  6. 检测同步模式 — 检查项目根目录中是否存在 .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 — 这是全新设置。

  7. 向用户报告所选模式:

    • syncMode = true:“找到现有解决方案 {uniqueName} v{version}。正在以同步模式运行 — 我将发现当前网站清单,与解决方案中已有的内容进行比较,并仅添加缺失的组件。”
    • syncMode = false:“未找到现有解决方案清单。正在进行全新设置 — 我将创建发布者和解决方案,然后添加所有网站组件。”
  8. 检查拆分计划(多解决方案模式) — 查找 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 搜索/获取出错,记录一条简短说明并继续 — 此技能必须能够离线运行。

  1. 使用查询 Power Pages solution publisher creation Dataverse component types ALM 运行 microsoft_docs_search。
  2. 通过 microsoft_docs_fetch 并行获取 https://learn.microsoft.com/en-us/power-platform/alm/solution-concepts-alm(如果搜索结果显示有相关的新教程,最多再获取一个相关页面 — 例如,多解决方案分层、托管属性指南)。
  3. 提取一段摘要,说明 Microsoft Learn 当前关于解决方案组件、发布者前缀不可变性、托管与非托管的选择,以及组件类型整数值的说明。与 ${PLUGIN_ROOT}/references/solution-api-patterns.md 进行比较,并标记任何差异(新增组件类型、操作签名变更、已弃用的模式)。
  4. 使用此摘要为阶段 2 及后续阶段的决策提供依据。不要悄悄更改技能行为 — 在阶段 4(创建发布者和解决方案)之前,向用户提示任何差异,但不强制中断流程。
阶段 2 — 收集解决方案配置

当 syncMode = true 时,跳过整个阶段。 改用清单中的 existingSolution.publisher 和 existingSolution.solution。跳转至阶段 5。

🚦 关卡(征求同意 · setup-solution:2.publisher-prefix): 发布者前缀是永久的,并会添加到每个组件逻辑名称的前面。必须获得明确确认。取消操作会在写入任何发布者/解决方案信息之前退出。

通过 AskUserQuestion 询问用户:

  1. 发布者唯一名称(例如 contoso)— 仅限小写字母/数字,不含空格。说明此名称是永久的,无法更改。
  2. 发布者显示名称(例如 Contoso)— 显示名称
  3. 发布者前缀(例如 con)— 2–8 个小写字母,会添加到所有组件前面。说明此名称是永久的,无法更改。
  4. 解决方案唯一名称(例如 ContosoSite)— 仅限字母/数字/下划线,不含空格
  5. 解决方案显示名称(例如 Contoso Site)— 显示名称
  6. 解决方案版本(默认值:1.0.0.0)— 必须采用 major.minor.build.revision 格式

展示所有值的确认摘要,并等待用户批准后再继续。

关键决策点:发布者前缀和发布者唯一名称都不可撤销 — 继续之前暂停并明确征求用户确认。

阶段 3 — 检查现有状态

当 syncMode = true 时,跳过整个阶段。 清单保证解决方案存在,并且我们已在阶段 1 步骤 6 中完成验证。

创建任何内容之前,检查发布者和解决方案是否已存在:

  1. 查询发布者:GET {envUrl}/api/data/v9.2/publishers?$filter=uniquename eq '{publisherUniqueName}'&$select=publisherid,uniquename,customizationprefix (没有专用的发布者脚本 — 直接查询 OData 终结点。)
  2. 使用 verify-solution-exists.js 检查解决方案是否存在:
    node "${PLUGIN_ROOT}/scripts/lib/verify-solution-exists.js" \
      --envUrl "{envUrl}" \
      --uniqueName "{solutionUniqueName}" \
      --token "{token}"
    
    将输出捕获为 JSON;检查 .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。

  1. 创建发布者(如果尚不存在):

    • 使用发布者正文向 POST {envUrl}/api/data/v9.2/publishers 发送请求
    • 从 OData-EntityId 响应标头中提取 publisherId
    • 如果失败:报告错误并停止(不要继续创建解决方案)
    • 此步骤必须在创建任何解决方案之前完成——每个解决方案正文都会绑定 [email protected]。此步骤必须串行执行,不得并行。
  2. 创建解决方案:

    单解决方案模式(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,不要传入过期令牌。

  3. 报告:“发布者 {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 = [],则跳过此查询。

  1. 查询此站点发布者拥有的连接引用:

    GET {envUrl}/api/data/v9.2/connectionreferences
      ?$filter=startswith(connectionreferencelogicalname,'{publisherPrefix}_')
      &$select=connectionreferenceid,connectionreferencelogicalname,connectionreferencedisplayname,connectorid
    
  2. 对于每个 Cloud Flow(来自查询 E),解析其 clientdata JSON(workflows({workflowId})?$select=clientdata),找出它使用的 connectionReferenceLogicalName。筛选查询 G.1 的结果,仅保留这些引用——它们是必须加入解决方案的引用。

  3. 在运行时解析连接引用的 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 — 凭据类credentialNeedsDecisionCREDENTIAL_REGEX(ConsumerKey|ConsumerSecret|ClientId|ClientSecret|AppSecret|AppKey|ApiKey|Password,不区分大小写)在步骤 5.4.C 显示带覆盖选项的批量提示 — 自动分类(默认 Secret/String)、全部设为 Secret、全部设为 String、全部跳过,或逐项选择凭据
2a — 含值的身份验证配置promoteToEnvVarAUTH_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)
  • 另提供以下选项:“全部提升为环境变量”和“全部保留为普通站点设置”

对于用户选择提升的每项设置:

  1. 使用 ${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 必须生成相同的架构名称;内联规则可能导致输出不一致。
  2. 使用解析出的架构名称创建 environmentvariabledefinition:
    node "${PLUGIN_ROOT}/scripts/lib/create-env-var-definition.js" \
      --envUrl "{envUrl}" \
      --token "{token}" \
      --schemaName "{schemaName from step 1}" \
      --displayName "{friendlyName}" \
      --type 100000000
    
    对于身份验证配置设置,请使用类型 100000000(String),而不是 Secret——这些是功能标志,不是凭据。将输出捕获为 JSON;提取 .definitionId 和 .schemaName。
  3. 记录 definitionId,以便将其添加到组件列表中(步骤 5.6,ComponentType: 380)。
  4. 将站点设置链接到环境变量,使用 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 设置。” 选项:

  1. 按名称自动分类 (推荐) — 应用第 5.4.C.1 步中的自动分类:{S} 作为 Secret 环境变量,{T} 作为 String 环境变量。只需确认一次,即可处理全部 {N} 项。(默认选项。)
  2. 全部作为 Secret 环境变量 — 将每项凭据都视为由 Key Vault 支持的 Secret 环境变量。此选项较为保守;适用于任何凭据,但会为实际上不需要的阶段值增加 Key Vault 依赖。
  3. 全部作为 String 环境变量 — 将每项凭据都视为纯文本的分阶段环境变量。仅当所有凭据都不是真正的机密(例如仅供内部使用的测试设置)时使用。
  4. 全部跳过 — 不将任何项添加到解决方案中。用户按环境在解决方案之外管理所有凭据值。等同于 IronItOut 之前的“excluded”行为。
  5. 逐项选择 — 对每项凭据分别提示,以便进行精细控制(每项设置可选 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 的项)。

对于每项选择使用环境变量处理的设置:

  1. 使用 ${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}"
    
  2. 使用解析出的架构名称创建 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 环境变量,将开发值设为默认值。

  3. 记录 definitionId,以便将其纳入组件列表(第 5.6 步,ComponentType: 380)。

  4. 使用 link-site-setting-to-env-var.js 将站点设置关联到环境变量(调用方式与上面的第 5.4.A 步相同)。

  5. 站点设置本身也会与环境变量定义一起添加到解决方案中——两者都是受跟踪的组件。

如果创建任何一个环境变量失败(例如循环中令牌过期、架构名称冲突等),请显示失败的设置名称和原因,并询问用户是重试、仅跳过该设置,还是中止整个批量操作。不得静默丢弃凭据。

向后兼容性:读取 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} 的环境变量定义。请选择要纳入的定义。仅包含定义——值仍按环境分别保存,不会随之迁移。

  1. {schemaName}({displayName})— 类型 {type},当前所属:{tag}
  2. ...

另外:包含所有仅属于 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} 的站点组件:

  1. {name}(类型 {type} {typeLabel})— 当前所在位置:{currentSolution}
  2. ...

另外:纳入所有孤儿(推荐) / 暂时跳过"

将用户的选择收集到 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 执行批量操作;其中内置了令牌刷新和幂等性处理。

组件数组应按以下顺序构建:

  1. 网站记录——{ componentId: websiteRecordId, componentType: websiteComponentType, addRequired: true, description: "Website: {siteName}" }
  2. 站点语言记录——每种语言一条记录,使用 siteLanguageComponentType(不会由 AddRequiredComponents 自动包含)
  3. 所有已确认的 powerpagecomponent 组——每个组件一条记录,使用 subComponentType
    • 表权限(类型 18)属于标准 powerpagecomponents,默认包含
    • 排除未转换为环境变量的 OAuth 密钥站点设置
  4. 环境变量定义——每条定义一条记录,使用 { componentType: 380 }。包括:
    • 在步骤 5.4(OAuth 密钥转换)中创建的所有环境变量
    • 步骤 5.4b 中 adoptedEnvVars 的每一项(用户选择包含的孤立项)
  5. Dataverse 表——{ componentType: 1, componentId: MetadataId }
  6. 已确认的云端流(来自步骤 5.5)——{ componentId: workflowId, componentType: workflowComponentType }(使用运行时发现的类型)
  7. 已确认的机器人组件——{ componentId: botId, componentType: botComponentType }(使用运行时发现的类型)
  8. 已确认的云端流使用的连接引用(来自步骤 5.2 查询 G)——每个引用一条记录:{ componentId: connectionReferenceId, componentType: connectionReferenceComponentType, addRequired: false }。如果 connectionReferences = [],则跳过。使用运行时解析的 connectionReferenceComponentType——不要硬编码(不同租户中观察到的值包括 10137 和 10160;该值因环境而异)。缺少这些记录时,解决方案可以顺利导出,但目标导入会因 MissingDependency 错误而失败——deploy-pipeline 阶段 6.6.1 会将其显示为“缺少连接引用”验证失败。
  9. 已采纳的孤立 ppc(来自步骤 5.4c)——{ componentId: ppc.id, componentType: subComponentType, addRequired: false }。使用步骤 5.1 中 discover-component-types.js 解析出的 subComponentType 值——不要硬编码。不要设置 DoNotIncludeSubcomponents: true——Dataverse 会拒绝在非 Entity 组件上使用此标志(HTTP 400 0x80040216)。

单解决方案模式(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):

  1. 将统一组件数组筛选为 Dataverse 类型名称映射到该解决方案 componentTypes 数组中的组件。数值型 componentType → 名称的映射,与 discover-component-types.js 和 discover-site-components.js 使用的映射相同(PPC_TYPE_LABELS)。表将路由到 tableLogicalNames 包含该表逻辑名称的解决方案(策略 3),或路由到声明了 'Table' 的解决方案(策略 1 和 2)。
  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 都相同。
  3. 如果某个组件的类型与任何解决方案的 componentTypes 都不匹配,则逐个组件发出警告并停止——划分过程丢失了组件。这通常意味着拆分计划遗漏了某种类型(compute-split-plan.js 中出现回归);用户需要重新规划,而不是让组件悄悄泄漏到 Default 中。

如果 helper 通过名称解析不够充分,则使用 Phase 4 中的 SOLUTIONS_BY_NAME 将每个 uniqueName 解析为对应的 solutionId。

该脚本每 20 次调用刷新一次令牌,将“已在解决方案中”视为成功,并输出 JSON 摘要 { total, success, skipped, failed, failures }。完成后删除临时文件。

阶段 6 — 验证并编写清单
  1. 验证组件:GET {envUrl}/api/data/v9.2/solutioncomponents?$filter=_solutionid_value eq '{solutionId}'&$select=objectid,componenttype
  2. 按类型统计组件,确认网站记录(使用 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 }。不要转换——渲染器会直接读取这些字段。

  1. 将 .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[]。
  2. 提交: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 询问用户接下来想做什么:

"你希望如何将此解决方案部署到其他环境?"

选项:

  1. “使用 Power Platform Pipelines(推荐)” — 在 PP Pipelines 宿主环境中设置管道;支持分阶段部署、审批关卡和各阶段的环境变量覆盖。
  2. “手动导出和导入” — 将解决方案导出为 zip 并直接导入目标环境。适用于一次性部署,操作更简单。
  3. “我稍后再决定” — 显示后续步骤建议并退出。

如果用户选择选项 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 是规划工具,用户会一次执行一个技能。

关键决策点(等待用户)

  1. 阶段 2:确认发布者前缀——这是永久设置,无法更改
  2. 阶段 3:确认复用还是创建——在进行任何写入之前
  3. 阶段 1,步骤 5:ALM 计划上下文——使用 plan-alm 预加载的网站设置分类,或重新发现并分类
  4. 阶段 5,步骤 5.4:包含值的身份验证设置——多选哪些设置要提升为环境变量,哪些保留为普通网站设置;对于凭据类型设置,逐项提示(Secret 环境变量 / String 环境变量 / 跳过)
  5. 阶段 5,步骤 5.5:完整清单审查——用户查看所有内容(网站、网站语言、所有组件类别、表、环境变量定义、authNoValue 警告),并在写入任何组件之前确认或调整
  6. 阶段 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,并提交
显示摘要正在显示摘要显示解决方案详情、组件数量和后续步骤

评论

支持 Markdown