mcpField
S
setup-offline-profile

setup-offline-profile

github.com/microsoft开发1.1k130 次安装今天更新
代码仓库

关于 setup-offline-profile

用于在 Power Apps 项目中配置 Dataverse Mobile Offline Profile 的工作流技能。它支持创建或扩展配置,检查表范围与关系,并为原生离线应用保存设置。

主要功能

  1. 01创建或扩展离线配置
  2. 02设置各表行范围
  3. 03配置列、关系与同步
  4. 04验证并发布配置
  5. 05保存设置并支持续跑

适用场景

  1. 01为原生 Power Apps 应用设置离线数据
  2. 02扩展现有 Dataverse 离线配置
  3. 03中断后继续配置流程

安装

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

$npx skills add https://github.com/microsoft/power-platform-skills --skill setup-offline-profile

安装后在 Claude Code 中输入/setup-offline-profile确认已加载。

详细介绍

插件检查:运行 node "${PLUGIN_ROOT}/scripts/check-version.js"——如果输出消息,请先向用户展示,再继续。

共享说明:shared-instructions.md — 请先阅读。

参考资料:

设置离线配置文件

用于创建 Dataverse Mobile Offline Profile 的端到端向导,供应用(以及任何其他兼容的 Power Apps 客户端)下载数据以供离线访问。

v0 范围:仅限配置。此技能会在现有的 Dataverse Mobile Offline Profile 表(mobileofflineprofile、mobileofflineprofileitem、mobileofflineprofileitemassociation)中创建记录,并将完整的应用级离线配置——配置文件元数据、每个表的范围以及临时 SDK 变通字段——写入 offline-profile.json。此技能不会修改 power.config.json(该文件由 pa app init 管理,其架构由上游控制)。模板已捆绑 @microsoft/power-apps-native-offline;@microsoft/power-apps-native-host 会使用该配置文件,并负责本地 SQLite 访问、排队同步、重新连接行为和状态 UX。此技能不会搭建重复的、由应用管理的离线运行时代码。

v0 不在范围内的内容:

  • 自定义筛选器模式(recorddistributioncriteria=3,已保存视图选择器)——推迟到 v0.5
  • 用户/团队成员分配——拆分到 /assign-offline-profile
  • 行数下载估算——拆分到 /preview-offline-scope

应用根目录绑定

进入向导前,以及读取任何项目或运行任何命令前,执行 app-working-directory.md。 解析出一个绝对路径 working_dir:继承子调用的所属根目录,或根据该契约使用直接调用显式指定的 --working-dir/初始 cwd。 缺失或冲突的子调用上下文应返回 NEEDS_CONTEXT,绝不能回退到启动 cwd。 将每次 shell 调用和文件工具(包括参考资料和重试)都绑定到此根目录;文件工具使用绝对的 <working_dir>/... 路径。

将所选环境 ID/URL/租户及提供的答案保留为调用上下文。每次新调用都要重新提供绝对路径 PLUGIN_ROOT 和必需值;之前的 shell 变量不会保留。将相同的根目录和身份信息传递给架构师和前置条件助手。入口审批并不意味着可以跳过向导的离线门槛。--plan-only 或处于规划阶段的调用方应在进入这个会修改数据的向导前返回 NEEDS_CONTEXT: offline setup requires an implementation-phase invocation。

工作流

  1. 验证项目和身份验证 → 2. 确定模式(创建或扩展)→ 3. 启动架构师 Agent → 门槛 1(表前置条件)→ 4. 如有需要,运行内部 enable-tables-offline 工作流 → 5. POST 配置文件外壳 → 门槛 2(每个表的行范围)→ 6. POST 配置文件项 → 门槛 3(关系 + 列 + 同步)→ 7. POST 关联 → 8. 验证并发布 → 9. 持久化构件 → 10. 摘要

步骤 1 — 验证项目和身份验证
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 || ''") || {
  echo "BLOCKED: unreadable power.config.json" >&2; exit 1;
}
if [ -z "$environment_id" ]; then
  echo "NEEDS_CONTEXT: selected app has no environmentId" >&2; exit 1;
fi
node "${PLUGIN_ROOT}/scripts/resolve-environment.js" "$environment_id" --no-cache --require-tenant

要求已解析的环境 ID、HTTPS URL 和租户与提供的所有者上下文一致。将它们记录为 <selected-environment-id>、<envUrl> 和 <tenantId>。如果不一致,则返回 NEEDS_CONTEXT;不得切换环境或账户以继续操作。每次 Dataverse 请求都要传入 --tenant-id "<tenantId>",包括复制的参考命令和重试。

使用绝对路径文件工具查找 <working_dir>/.datamodel-manifest.json 或 <working_dir>/docs/plan-artifacts/.datamodel-manifest.json。在调用上下文中保留选定的清单绝对路径,供架构师(步骤 3)和工件更新(步骤 9)使用;切勿搜索其他应用。

仅 Web 目标检测 — Mobile Offline Profiles 仅适用于原生目标(iOS/Android)。如果项目仅面向 Web,配置文件仍会在 Dataverse 中创建,但生成的应用永远不会使用它:

cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
# Inspect platforms declared in app.config.js
node -e "
const c = require('./app.config.js');
const platforms = c?.expo?.platforms ?? [];
const hasNative = platforms.includes('ios') || platforms.includes('android');
console.log(JSON.stringify({ platforms, hasNative }));
"
hasNative操作
true(包含 ios 和/或 android)正常继续
false(仅 Web 或没有平台)打印:⚠ This project only targets web — Mobile Offline Profiles don't apply (they require iOS/Android). Continuing will create the profile in Dataverse but no app will use it. 使用 AskUserQuestion 询问:“仍要继续吗?”默认选择“否”。
解析错误 / app.config.js 缺少键发出警告,但假定为原生项目(不要因解析器问题而阻止操作)

停止条件:

  • 没有 power.config.json → “请先运行 /create-mobile-app。”
  • 既没有 .datamodel-manifest.json,也没有 docs/plan-artifacts/.datamodel-manifest.json → “请先运行 /add-dataverse——离线配置文件需要数据模型。”
  • 环境解析失败 → 按标准流程恢复身份验证(使用 az login --tenant <env-tenant> 或直接提供环境 URL;参见 shared-instructions.md)。
  • 仅面向 Web + 用户拒绝覆盖 → 停止。打印:Offline profile creation skipped — no native target.
步骤 1a — 环境一致性检查

与 /add-dataverse 步骤 3a 相同——验证 power.config.json 能否解析,以及 az 是否能为目标租户获取令牌。如果不能,则停止;用户必须使用 az login --tenant <env-tenant> 重新进行身份验证。

步骤 1b — 恢复检查

读取 memory-bank.md 中的 ## Offline profile 区块。根据 status 判断:

status 值操作
(区块不存在)或 status: none首次运行。继续执行步骤 2。
status: not-applicable用户之前在 /create-mobile-app 离线配置文件设置期间选择了退出(“不需要离线支持”)。重新确认:“Memory-bank 显示此应用不需要离线支持。是否覆盖此设置并继续?(y/N)”。默认选项 N 会在此处停止。
status: done 且环境中仍存在与 profileId 匹配的配置文件已完成。从 memory-bank 区块打印摘要;询问用户是否要运行 /edit-offline-profile(v0.2)或直接退出。
status: done,但 GET /mobileofflineprofiles(<profileId>) 返回 404配置文件已在外部删除(通过 maker portal 或其他环境)。视为 none;清除该区块;继续执行步骤 2。
status: in-progress 且环境中存在配置文件恢复流程——见下文。
status: in-progress,但环境中不存在配置文件Memory-bank 已过时。自动清理:清除该区块,记录 recovered from stale in-progress state,然后作为全新运行继续执行步骤 2。

恢复流程——当 memory-bank 中的 status: in-progress 且配置文件仍然存在时:

  1. 执行 GET /mobileofflineprofiles(<profileId>)?$expand=MobileOfflineProfile_MobileOfflineProfileItem,计算实际已提交的内容:

    • 0 个项 → 配置文件外壳已创建,但尚未 POST 项。从步骤 6 恢复。
    • 1 个或更多项,但清单中仍有缺失项 → 从步骤 6 恢复,跳过已存在的项。
    • 所有项均已存在,但至少 1 项的 selectedcolumns 为空 → 从步骤 7(PATCH)恢复。
    • 所有项均已存在,且每项都有 selectedcolumns → 从步骤 8(发布)恢复。
    • componentstate=0(已发布)→ memory-bank 记录有误;视为 done。
  2. 向用户提出一个合并的 AskUserQuestion(而不是逐步请求批准):

    Resume from <computed step> on profile "<name>" (<profileId>)?
    
    Items already committed: <N> of <M>
    PATCHes already applied: <K>
    Published:               <yes|no>
    
    Options:
    - Resume from where it left off (recommended)
    - Start fresh — delete the half-built profile and re-run from Step 5
    - Cancel
    
  3. 选择 Resume → 跳转至计算出的步骤。通过匹配 selectedentitytypecode 跳过已提交的项。

  4. 选择 Start fresh → 执行 DELETE /mobileofflineprofiles(<profileId>)(级联删除项和关联),清除 memory-bank 区块,然后作为新运行继续执行步骤 2。

  5. 选择 Cancel → 停止,不更改 memory-bank。

**Memory-bank 检查点约定:**技能会在步骤 5(配置文件外壳 POST)后立即写入 status: in-progress 和 profileId。之后每个步骤都会更新 lastSuccessfulStep: 字段,以便恢复时确定接续位置。任何步骤返回 BLOCKED: 时,技能都会保留 status: in-progress;返回 DONE 时,则在步骤 9c 中写入 status: done。

**POST 重试的幂等性:**如果步骤 6 针对同一父配置文件,以相同的 selectedentitytypecode 再次 POST 某个项,Dataverse 可能返回 409 Conflict(“duplicate”)。包装器 dataverse-request.js 会通过其 looksLikeDuplicate 逻辑将此视为静默成功——可安全重试。

步骤 2 — 确定模式(创建、扩展还是协调)

遥测检查点:resolve_offline_profile_mode

开始前打印:

"→ 正在检查环境中是否存在现有离线配置文件…"

cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
node "${PLUGIN_ROOT}/scripts/dataverse-request.js" <envUrl> GET \
  "mobileofflineprofiles?\$select=mobileofflineprofileid,name,description,publishedon" \
  --tenant-id "<tenantId>"

决策树 — 按顺序评估:

#条件操作
1步骤 1b 已在 memory-bank 中检测到 status: in-progress恢复流程(已在步骤 1b 中处理)。此处不要重新评估。
2offline-profile.json 的顶层包含 profileId: <X>,且环境中仍存在配置文件 <X>冲突情况 — 询问用户。 AskUserQuestion:(a)扩展固定的配置文件(根据当前数据模型重新设计,并按需添加/PATCH 项目);(b)删除固定的配置文件并重新创建(不可撤销 — 级联删除项目、关联和成员关系);(c)取消。默认 =(a)扩展。绝不默默删除。
3offline-profile.json 的顶层包含 profileId: <X>,但配置文件 <X> 不存在(GET 返回 404)offline-profile.json 已过时(环境已重置或配置文件已手动删除)。删除本地文件,继续处理第 4 行。
4环境中的任意现有配置文件,其 name 与此应用的名称匹配(对 power.config.appDisplayName 或目录名称进行不区分大小写的子字符串匹配)询问用户。 AskUserQuestion:(a)扩展名称匹配的配置文件;(b)创建新配置文件(名称匹配的配置文件可能属于其他应用,由你决定);(c)取消。无默认选项 — 两种选择都合理。
5环境中有 0 个配置文件模式 = create-new。继续步骤 3。
6环境中有 1 个或更多配置文件,但没有名称匹配项,也没有固定配置文件模式 = create-new(环境中存在无关配置文件)。继续步骤 3。打印一行提示:↷ 环境中存在 <N> 个无关配置文件;不会扩展 — 请参阅 /edit-offline-profile 管理它们。

第 2 行为何至关重要(2026-05-25 的实测结果):chanel-rm 和 FCB Tracker 测试运行遇到了此情况。没有这项检查时,技能会在不通知用户的情况下级联删除固定的配置文件。应用此补丁后,用户会看到明确的三选一选项,且 delete 始终是显式操作。

扩展模式(第 2a、4a 行)的实现:使用 Mode: extend, existingProfileId: <X> 重新启动架构师。架构师会将当前方案与服务器上的项目和关联进行比较,并输出三个列表:(i)要添加的项目;(ii)要 PATCH 的项目(范围/列/同步差异);(iii)要删除的项目(存在于服务器上但不在当前数据模型中)。在 Gate 2 向用户展示。获批后,技能执行增量写入 — 不发送 profile-shell POST。

步骤 3 — 启动架构师 Agent

遥测检查点:design_offline_profile_scope

开始前打印:

"→ 正在启动 mobile-app:offline-profile-architect Agent(只读)以设计配置文件…"

通过 Task 启动:

agent: mobile-app:offline-profile-architect
prompt:
  Owner: setup-offline-profile
  Phase: planning
  Working directory: <working_dir>
  Plugin root: ${PLUGIN_ROOT}
  Environment ID: <selected-environment-id>
  Environment URL: <envUrl>
  Tenant ID: <tenantId>
  Manifest path: <absolute manifest path within working_dir>
  Proposal output: <working_dir>/_offline_section.md
  Publisher prefix: <prefix>
  Mode: default

该 Agent 会在工作目录中返回 _offline_section.md。读取该文件,并解析第一行的状态代码:

  • DONE → 继续
  • DONE_WITH_CONCERNS: <list> → 在相关的 Gate 2 / Gate 3 阶段向用户说明顾虑;继续
  • NEEDS_CONTEXT: <missing> → 补足缺失的上下文(最常见的情况是缺少数据模型文件),然后重新启动一次(最多重试 2 次)。如果仍然失败,则停止并说明 Agent 给出的原因。
  • BLOCKED: <reason> → 停止,向用户说明,不要默默重试。
步骤 3.5 — 配置审查(交互式 AskUserQuestion 流程)

遥测检查点:review_offline_profile_configuration

开始前打印:

"→ 正在展示拟议的离线配置文件配置。点击一个选项即可接受、调整或取消 — 无需输入文字。"

技能会将方案呈现为单屏摘要(只读上下文),然后通过结构化的 AskUserQuestion 提示引导用户做出选择 — 与 /create-mobile-app 用于其 4 个计划关卡的点击式交互模式相同。这取代了旧的“输入 accept / 用自然语言编辑 / cancel”回复流程;当用户输入解析器无法识别的内容时,该流程经常无法正常工作。

步骤 3.5a — 打印摘要(仅供参考)

从 _offline_section.md 和架构师的输出中代入实际值:

══════════════════════════════════════════════════════════════════════
 Mobile Offline Profile — Proposed Configuration
══════════════════════════════════════════════════════════════════════

PROFILE METADATA
  Name        : <proposed name>
  Description : <proposed description, or "(none)">
  Mode        : create-new | extend existing (<profileId>)

TABLE PREREQUISITES (IsAvailableOffline + ChangeTrackingEnabled)
  cr123_note   ❌/❌  → will be enabled
  cr123_visit  ✅/❌  → ChangeTracking will be enabled
  contact      ✅/✅  → no change

PER-TABLE CONFIG

  ▸ cr123_note (Organization rows, User's rows)
      Sync     : every 10 min
      Columns  : 12 (cr123_title, cr123_body, cr123_visitid, …, modifiedon, createdon)

  ▸ cr123_visit (Related rows only)
      Sync     : every 10 min
      Columns  : 8 (cr123_name, cr123_date, …, modifiedon)

  ▸ contact (All records)
      Sync     : every 60 min
      Columns  : 5 (fullname, emailaddress1, …, modifiedon)

RELATIONSHIPS (will be POSTed as mobileofflineprofileitemassociation rows)
  cr123_note   → cr123_note_visit   (links to cr123_visit)
  cr123_note   → cr123_note_image   (includes image bytes)
══════════════════════════════════════════════════════════════════════

此文本旁边不会显示任何提示词——它仅提供参考信息,后面会立即显示交互式提示。

步骤 3.5b — 顶层决策(单个 AskUserQuestion)
header   : Offline profile
question : "Accept the proposed offline profile configuration and publish to Dataverse?"
options  :
  - Accept and publish (Recommended)
  - Adjust before publishing
  - Cancel — abort, nothing is mutated

结果:

用户选择操作
接受并发布写入 memory bank 检查点(见下文),继续执行步骤 4(如有需要则启用先决条件)→ 步骤 5(POST shell)→ 步骤 6(POST items)→ 步骤 7(PATCH selectedcolumns)→ 步骤 8(发布)→ 步骤 9(持久化)→ 步骤 9.5(验证)→ 步骤 10(摘要)。除非发生故障,否则不再显示提示。
取消停止。不对 Dataverse 进行任何修改。不写入任何工件。更新 memory-bank.md,设置 status: cancelled-at-config-review。
发布前调整继续执行步骤 3.5c。
步骤 3.5c — 选择要调整的区域(单个 AskUserQuestion,multiSelect)
header     : Adjust which
question   : "Which sections of the proposal do you want to change?"
multiSelect: true
options    :
  - Profile name or description
  - Per-table row scope (who's rows get synced)
  - Per-table sync interval
  - Per-table column selection

用户选择的内容决定步骤 3.5d 的执行。如果用户未选择任何内容(关闭对话框),则视为无操作,并返回步骤 3.5b。

步骤 3.5d — 按区域逐项深入调整

对用户在步骤 3.5c 中选择的每个区域,运行相应的子流程。过程中将编辑应用到内存中的配置副本;在步骤 3.5e 之前,不会对 Dataverse 执行任何操作。

配置文件名称或说明 — 文本提示(自由输入,无法列出选项):

“输入新的配置文件名称,或输入 skip 保留 <current name>。”

然后:“输入新的说明,或输入 skip 保留 <current description>。”

验证名称不超过 100 个字符;去除首尾空格;拒绝空值。

每个表的行范围 — 每个表显示一个 AskUserQuestion(每次调用最多批处理 4 个表;5 个或更多表时分多次调用):

header   : Scope <table>
question : "Row scope for `<table-logical-name>`?"
options  :
  - Related rows only (child of a parent table; default for pure children)
  - All records (default for shared catalogs like product / contact)
  - User's own rows (Organization-scoped + recordsownedbyme=true; default for personal transactional data)
  - Organization's rows (Organization-scoped, all owners — broadest non-All)

将答案映射到 recordDistributionCriteria(0=Related,1=All,2=Organization)以及 recordsownedby* 子标志。如果用户选择“用户自己的行”,则设置 recordsownedbyme=true;如果选择“组织的行”,则将三个子标志全部设为 false。

每个表的同步间隔 — 每个表显示一个 AskUserQuestion:

header   : Sync <table>
question : "How often should `<table-logical-name>` sync?"
options  :
  - Every 5 min — hot transactional data (Recommended for tables users edit live)
  - Every 15 min — typical
  - Every 30 min — slower-moving
  - Every 60 min — static catalogs (lowest battery cost)

(服务端范围为 5–1440 分钟;这四个选项之外的自定义值通过 AskUserQuestion 始终提供的 Other 自由文本选项输入——验证 5 ≤ N ≤ 1440。)

每个表的列选择 — 文本回退方案(列列表变化太多,不适合使用多选):

“对于 <table>,请每行输入一项列变更: 输入 exclude <column>,从同步中移除某列 输入 include <column>,添加当前已排除的列 输入 all,重置为清单中的所有列 输入 skip,保持列不变”

验证每个指定的列是否存在于 .datamodel-manifest.json 中;如果存在拼写错误,则拒绝并提供最接近的列名建议。

步骤 3.5e — 重新确认

完成所有选定的子流程后,重新渲染摘要(采用步骤 3.5a 的格式),并使用不同颜色或在行首添加 *,标记已更改的行 → updated。然后重新运行步骤 3.5b——使用相同的三个选项 AskUserQuestion。用户可以再次调整、接受或取消。

此循环以用户的耐心为限,没有硬性限制。如果用户反复选择“调整”,始终不选择“接受”或“取消”,这是用户的权利——每次循环都可撤销,且尚未对 Dataverse 执行任何操作。

第 3.5f 步 — Memory bank 检查点(在 Accept and publish 之后)
## Offline profile
status: in-progress
profileName: <name>
mode: create-new | extend
configReview: accepted

设计理由。 原来的第 3.5 步使用自由文本回复(“输入 accept 或用英文描述修改内容”)来保持对话感。实际使用中,用户输入的回复无法被正则解析器识别,例如 change scope of contact to teamonly、set sync to 10 等,导致技能要么悄悄丢弃修改,要么提出澄清问题,引发更多困惑。上面的 AskUserQuestion 流程消除了可枚举字段(范围、同步间隔、顶层决策)的解析风险,同时保留自由文本路径,用于真正自由形式的字段(名称、描述、列列表)。最终效果:常见调整不再出现含糊不清的交互,减少因输入导致的错误,并与 /create-mobile-app 的计划确认门 UX 保持一致。

第 4 步 — 如有需要,运行内部 enable-tables-offline 工作流

遥测检查点:enable_dataverse_tables_offline

如果 Gate 1 识别出需要更改的表,请读取并执行 ${PLUGIN_ROOT}/skills/enable-tables-offline/SKILL.md,并提供已批准的表列表 以及下方完整的限定范围交接信息。配置审查必须已获接受;已保存的清单或单独的表列表均不构成批准。

$ARGUMENTS: --working-dir '<working_dir>' <comma-separated approved logical names>

Context:
  MOBILE_APP_ORCHESTRATING=1
  orchestrator: setup-offline-profile
  working_dir: <working_dir>
  phase: implementation
  approved_scope:
    environmentId: <selected-environment-id>
    environmentUrl: <envUrl>
    tenantId: <tenantId>
    tables: <exact Gate 1-approved prerequisite table allowlist>
    operations: <approved IsAvailableOffline/ChangeTrackingEnabled changes>
    publication: <approved targeted publication; broader fallback only if separately approved>
  supplied_answers: <current offline gate responses>

等待其返回并解析第一行的原样状态。DONE 表示继续; DONE_WITH_CONCERNS 必须在继续前提请关注。遇到 NEEDS_CONTEXT 时, 请在此所有者已批准的根目录/范围内补齐缺失的上下文,或返回阻塞项;绝不能使用不同的应用重新分派,也不能推断已获批准。

如果状态为 BLOCKED,向上报告阻塞项 — 停止。

如果所有表都已启用,则跳过此步骤。

第 5 步 — POST 配置文件外壳

遥测检查点:create_offline_profile_shell

开始前打印:

"→ 正在创建 MobileOfflineProfile 记录(仅包含 Name 和 Description)…"

对于 create-new 模式:

cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
node "${PLUGIN_ROOT}/scripts/dataverse-request.js" <envUrl> POST \
  "mobileofflineprofiles" \
  --tenant-id "<tenantId>" \
  --body '{
    "name": "<app name> Offline Profile",
    "description": "<auto-generated description referencing app name + scope summary>"
  }' \
  --include-headers

从 OData-EntityId 响应头中提取 mobileofflineprofileid(与 /add-dataverse 第 5b 步中的模式一致)。

对于 extend 模式:重用第 2 步中的现有 mobileofflineprofileid。

写入 memory-bank.md:

## Offline profile
status: in-progress
profileId: <guid>
profileName: <name>
mode: create-new | extend
gate1: approved

###(Gate 2 — 已移除,已合并至第 3.5 步)

第 6 步 — POST 配置文件项

遥测检查点:add_tables_to_offline_profile

开始前打印:

"→ 正在创建 条 MobileOfflineProfileItem 记录(每张表一条,依次创建)…"

⚠️ 并发规则。 配置文件项通过父配置文件隐式地相互关联。请依次逐条发送 POST 请求。mobileofflineprofileitem 实体不会像 EntityMetadata 那样持有元数据锁,但每个 POST 请求的验证过程都会读取相邻项 — 并行 POST 请求偶尔会返回 412 PreconditionFailed。依次执行是安全的方式。

按顺序处理每张表:

cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
node "${PLUGIN_ROOT}/scripts/dataverse-request.js" <envUrl> POST \
  "mobileofflineprofileitems" \
  --tenant-id "<tenantId>" \
  --body '{
    "name": "<table display name>",
    "[email protected]": "/mobileofflineprofiles(<profileId>)",
    "selectedentitytypecode": "<table-logicalname>",
    "recorddistributioncriteria": <0|1|2>,
    "recordsownedbyme": <bool>,
    "recordsownedbymyteam": <bool>,
    "recordsownedbymybusinessunit": <bool>,
    "getrelatedentityrecords": true,
    "syncintervalinminutes": 10
  }' \
  --include-headers

从响应头中提取每个 mobileofflineprofileitemid。存储在本地映射 tableLogicalName → itemId 中 — 第 7 步(关联)和第 9 步(offline-profile.json)需要用到。

此处不设置 selectedcolumns — 在 Gate 3/第 7 步中,用户确认列子集后再添加。

每个 2xx 响应后打印 ✓ <table>。

###(Gate 3 — 已移除,已合并至第 3.5 步)

第 7 步 — POST 关联项 + PATCH selectedcolumns

遥测检查点:configure_offline_profile_associations

开始前打印:

"→ 正在创建关联行,并对每个配置文件项执行 PATCH 以设置 selectedcolumns…"

步骤 7a — POST mobileofflineprofileitemassociation 行

2026-05-24 + 2026-05-25 的实测捕获证实,maker portal 已取消关联创建限制。操作方法(不包含 selectedrelationshipsschema 字段——由服务器填充):

cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
node "${PLUGIN_ROOT}/scripts/dataverse-request.js" <envUrl> POST \
  "mobileofflineprofileitemassociations" \
  --tenant-id "<tenantId>" \
  --body '{
    "name": "<relationshipSchemaName>",
    "relationshipdisplayname": "<relationshipSchemaName>",
    "relationshipid": "<MetadataId-of-1:N-relationship-from-architect>",
    "[email protected]": "/mobileofflineprofileitems(<PARENT-item-id>)"
  }' \
  --include-headers

⚠️ 关键方向规则。 regardingobjectid 是**父项(1 侧)**配置文件项。对于 account → orderline 1:N 关系,关联位于 account 配置文件项上,关系为 account_orderline(从 EntityDefinitions(LogicalName='account')/OneToManyRelationships 读取)。在 orderline(子项)侧 POST 会导致 PublishXml 失败,并返回 0x80071140 — no relationships are specified for this Related-only table。这是典型错误——架构师的步骤 5 现在会明确遍历父项的 OneToManyRelationships,以生成以父项为键的输出。

对于架构师提案中的每一对(父项、指向子项的 1:N 关系):

  1. 通过 EntityDefinitions(LogicalName='<PARENT-table>')/OneToManyRelationships,筛选 SchemaName === <relationshipSchemaName>,解析 relationshipid(MetadataId GUID)。按父表缓存查找结果。
  2. POST 该关联(依次执行——并行 POST 偶尔会返回 412 PreconditionFailed)。
  3. 从 OData-EntityId 响应标头中获取新建的 mobileofflineprofileitemassociationid。

冗余时跳过规则。 如果父配置文件项的 recorddistributioncriteria=1(所有记录),架构师应该已按步骤 5 的精简规则移除该项上的关联。为确保安全:如果仍有任何关联传递过来,请在 POST 时跳过。输出:↷ Skipping <association> on All-records parent <table> — redundant, all rows download anyway。

发布前的循环检测。 发布步骤 8 之前,根据刚创建的关联构建关系图,并使用 DFS 检测循环。如果存在循环(例如 account → task → account),请提示用户移除一个关联,并针对受影响的关系对重新执行此步骤;否则步骤 8 会返回 0x80071141。请参阅 shared/references/dataverse-offline-api.md §6b。

幂等性。 对已有的(项、关系)组合重新 POST 会返回 409 Conflict;dataverse-request.js 的 looksLikeDuplicate 补救逻辑会将其视为成功。部分失败后重新运行是安全的。

如果架构师的提案中不包含任何关系(少见——通常是夹具级的单表配置文件),则完全跳过步骤 7a。

步骤 7b — PATCH 每个项的 selectedcolumns
cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
node "${PLUGIN_ROOT}/scripts/dataverse-request.js" <envUrl> PATCH \
  "mobileofflineprofileitems(<itemId>)" \
  --tenant-id "<tenantId>" \
  --body '{
    "selectedcolumns": "<JSON string — see below>"
  }'

selectedcolumns 格式(通过检查 maker portal 创建的现有配置文件确认——注意事项请参阅计划中的 §10 已知阻塞项):

{
  "Columns": ["cr123_title","cr123_body","cr123_visitid","modifiedon","createdon","statecode"]
}

在 memo 字段中将其包装为 JSON 字符串。

如果在第 3 个关卡修改了 syncintervalinminutes,请在同一个 PATCH 中包含该字段。

步骤 8 — 发布

遥测检查点:publish_offline_profile

开始前输出:

"→ Publishing profile (targeted PublishXml)…"

有意跳过 Validate 步骤。 2026-05-17 的实测结果:服务器拒绝了文档中描述的 Validate 操作请求体格式。maker portal 也不会调用 Validate(已通过 2026-05-24 的捕获确认)——发布时的验证会在服务器端作为 PublishXml 的一部分运行。isvalidated=true 由发布调用本身设置,而非通过单独的 Validate 操作设置。

定向 PublishXml——maker portal 使用的模式(2026-05-24 实测):

cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
node "${PLUGIN_ROOT}/scripts/dataverse-request.js" <envUrl> POST \
  "PublishXml" --tenant-id "<tenantId>" --body '{
    "ParameterXml": "<publish><mobileofflineprofiles><mobileofflineprofile><profileId></mobileofflineprofile></mobileofflineprofiles></publish>"
  }'

只发布此配置文件,不会发布整个组织的自定义项。在共享环境中,实测速度快得多,也更不容易触发速率限制。 对于下面的修复辅助工具,请使用协调参考中的 限定范围的辅助工具交接,设置 orchestrator: setup-offline-profile,使用相同的绝对 working_dir、所选环境/租户/配置文件,以及用户精确批准的修复范围。传入 --working-dir '<working_dir>' 和所选配置文件/表参数。 不要从原始配置文件创建批准中推断出新的修复批准。

**遇到 400 / 0x80071141“循环关系”时:**配置文件的关联图存在环(例如 account → task → account)。从错误消息中解析路径,提示用户删除其中一条相关关联,然后通过 DELETE /mobileofflineprofileitemassociations(<id>) 重新删除该关联行,再次尝试发布。参见 shared/references/dataverse-offline-api.md §6b。

**对于 Related-only 表遇到 400 / 0x80071140“未指定关系”时:**某个配置文件项的 recorddistributioncriteria=0,但没有关联指向它。从错误消息中解析表名,并向用户提供两个选项:

  1. /add-table-to-offline-profile --table <name> --add-associations — 重新运行架构设计器以发现关系,并 POST 缺失的关联。
  2. /edit-offline-profile --table <name> --scope 2 --me — 将范围更改为组织行 + 用户的行(架构设计器的回退选项)。

对于全新的 /setup-offline-profile 运行,此错误不应出现,因为步骤 7a 会在步骤 8 发布之前 POST 关联。但在改造场景中,如果某个配置文件根据 v0.1 旧版“无关联”方案发布,之后又被编辑,则确实可能出现此错误——Dataverse 的发布验证器似乎会与之前发布的快照比较,而不是与当前未提交的状态比较。2026-05-24 在 chanel-rm 上有实测记录。

**回退方案 — PublishAllXml:**仅当定向发布无法完成,且用户另行批准发布所有待处理环境自定义项时使用。针对配置文件范围的批准、超时或速率限制,不代表用户同意扩大发布范围。否则应报告失败,不得扩大范围。

cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
node "${PLUGIN_ROOT}/scripts/dataverse-request.js" <envUrl> POST \
  "PublishAllXml" --tenant-id "<tenantId>" --body '{}'

处理“超时但成功”模式——在共享环境中有实测记录(CRM527116 + chanel-rm 演示):PublishAllXml 会触发 4 次重试的 429 退避,dataverse-request.js 在约 2 分钟后于客户端超时,但发布已在服务器端提交。定向 PublishXml 在大多数情况下应可避免此问题,但回退路径仍需处理此情况。

POST 调用返回后的处理流程:

  1. 如果状态为 204 → 发布成功,继续。

  2. 如果状态为 0,且错误包含 Request timed out 或 429 rate-limited → 不要视为失败。运行下面的验证 GET:

    cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
    node "${PLUGIN_ROOT}/scripts/dataverse-request.js" <envUrl> GET \
      "mobileofflineprofiles(<profileId>)?\$select=componentstate,publishedon" --tenant-id "<tenantId>"
    
    • componentstate == 0,且 publishedon 是过去 5 分钟内的非空 ISO8601 时间戳 → 视为成功。打印:↷ PublishAllXml client timed out but committed server-side (componentstate=0, publishedon=<ts>). 继续。
    • componentstate == 1(未发布)→ 发布确实未生效。等待 30 秒后重试一次(让正在进行的环境发布完成)。若第二次仍失败,则返回 DONE_WITH_CONCERNS: publish timed out twice; profile saved but unpublished — re-run /setup-offline-profile or PublishAllXml manually。
  3. 如果状态不是 204 且不属于以上情况 → 返回 BLOCKED: publish failed with <status>。

确认成功后,重新 GET 配置文件,并在制品步骤中检查 publishedon。

步骤 9 — 持久化制品

遥测检查点:persist_offline_profile_snapshot

开始前打印:

“→ 正在写入 offline-profile.json + memory-bank.md…” 重要:此技能绝不能修改 power.config.json。 该文件由 pa app init 管理,其架构由上游工具控制——在其中添加自定义字段可能会在重新初始化时被覆盖,并且会在上游架构变更时导致故障。所有应用级离线配置都应写入我们管理的 offline-profile.json 中的 appConfig 块。

9a — 在项目根目录写入 offline-profile.json。规范架构(必须符合 scripts/verify-offline-profile.js 的预期;该脚本通过 relationshipId 比较关联):

{
  "profileId": "<guid>",
  "name": "<name>",
  "publishedOn": "<iso8601>",
  "mode": "create-new | extend",
  "appConfig": {
    "enabled": true,
    "serverRowLimit": 2000,
    "/* BEGIN OFFLINE-CONFIG-WORKAROUND */": "remove when SDK gaps are fixed upstream — tracked in microsoft/power-platform-skills/plugins/mobile-apps/template#main (instanceUrl) + PowerApps-Client (useDda + entitiesIncluded). Bracket exists so the future cleanup diff is mechanical.",
    "useDda": false,
    "entitiesIncluded": ["<logicalName-1>", "<logicalName-2>", "..."],
    "instanceUrl": "<envUrl>",
    "/* END OFFLINE-CONFIG-WORKAROUND */": ""
  },
  "tables": [
    {
      "logicalName": "cr123_order",
      "itemId": "<guid>",
      "recordDistributionCriteria": 2,
      "recordsOwnedByMe": true,
      "recordsOwnedByMyTeam": false,
      "recordsOwnedByMyBusinessUnit": false,
      "syncIntervalInMinutes": 10,
      "selectedColumns": ["cr123_ordernumber", "cr123_total", "..."],
      "schemaColumns": ["cr123_ordernumber", "cr123_total", "cr123_notes", "..."],
      "relationships": [
        {
          "schemaName": "cr123_order_cr123_orderline",
          "relationshipId": "<MetadataId-guid-of-the-1:N-relationship>",
          "targetEntity": "cr123_orderline",
          "associationId": "<mobileofflineprofileitemassociationid-guid>"
        }
      ]
    }
  ]
}

架构不变量(由验证脚本强制执行):

  • profileId 是已发布 mobileofflineprofileid 的真实来源——运行时、/edit-offline-profile 和 /assign-offline-profile 都从此处读取。也不要将其写入 power.config.json。
  • appConfig 包含运行时在启动时使用的所有应用级离线设置。由 OFFLINE-CONFIG-WORKAROUND 括起来的三个字段是临时字段,并已跟踪以便从上游移除——请保持它们连续排列,以便将来通过 git diff 显示清理内容时仅涉及该代码块。
  • relationships[] 位于父表条目上(即 1:N 关系中处于 1 侧的表)。纯子表(例如 cr123_orderline)的 relationships: [] 为空。
  • 每个 relationships[] 条目包含 schemaName(来自 EntityDefinitions 元数据中该关系的 SchemaName)、relationshipId(该关系的 MetadataId GUID——这是规范比较键,在服务器端 relationshipname 格式发生变化时仍保持稳定)、targetEntity(子实体逻辑名称)和 associationId(已创建的 mobileofflineprofileitemassociationid)。
  • recordDistributionCriteria=1(所有记录)的父表始终使用 relationships: []——添加关联是多余的(参见架构师第 5 步的裁剪规则)。
  • schemaColumns[] 是架构对账基线——创建此项时该表架构中存在的完整列逻辑名称集合(来自 .datamodel-manifest.json)。它与 selectedColumns 不同,后者是运行时同步的精选子集:schemaColumns 记录对账时架构中的所有内容,包括有意排除在 selectedColumns 之外的列。scripts/offline-profile-delta.js 会将后续清单列与此基线进行比较,以检测真正新增的架构列(manifest.columns − schemaColumns),避免将有意排除的列误报。请从清单中每个表对应的条目填充此字段;如果清单未列出某个复用表的列,则写入 []。参见 offline-profile-reconciliation.md。
  • JSON 不支持注释——上面的 /* BEGIN/END OFFLINE-CONFIG-WORKAROUND */ 键只是为技能作者示意的分隔标记。不要将这些字面键写入 offline-profile.json。 技能实际修改文件时,只在 appConfig 中输出真实字段(useDda、entitiesIncluded、instanceUrl),并保持它们连续排列。

示例 Node 脚本(一次性写入文件——不对 power.config.json 执行读取-修改-写入):

cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
node -e '
  const fs = require("fs");

  // Genuine app-level offline config (always written)
  const appConfig = {
    enabled: true,
    serverRowLimit: 2000,
  };

  // BEGIN OFFLINE-CONFIG-WORKAROUND (remove when underlying SDK gaps are fixed
  // upstream — tracked separately in plugins/mobile-apps/template and PowerApps-Client).
  appConfig.useDda = false;
  appConfig.entitiesIncluded = [/* logicalName for each table in the profile */];
  appConfig.instanceUrl = "<envUrl>";
  // END OFFLINE-CONFIG-WORKAROUND

  const profile = {
    profileId: "<guid-from-step-5>",
    name: "<name>",
    publishedOn: "<iso8601>",
    mode: "create-new", // or "extend"
    appConfig,
    tables: [/* one entry per profile item — see schema above */],
  };

  fs.writeFileSync("offline-profile.json", JSON.stringify(profile, null, 2) + "\n");
'

字段说明:

字段层级原因
profileIdA(真实字段)来自 Dataverse 的已发布 mobileofflineprofileid——运行时会获取此配置文件的清单。offline-profile.json 是此值的唯一事实来源。
appConfig.enabledA(真实字段)将项目标记为已启用离线功能;由运行时在启动时使用。
appConfig.serverRowLimitA(真实字段)对应图 2 的“数据行限制”字段(默认值为 2000)。在 v0 中,此值仅用于配置——运行时强制执行属于 B 层的工作。
appConfig.useDdaOFFLINE-CONFIG-WORKAROUND强制 SDK 绕过有问题的 shouldUseDda 门控(实体集与逻辑名称不匹配)。当 @microsoft/react-native-dataverse-offline 中的该门控完成协调后移除此项(在 PowerApps-Client 中跟踪)。
appConfig.entitiesIncludedOFFLINE-CONFIG-WORKAROUNDSDK 的离线引导流程会读取此列表,以确定哪些实体逻辑名称属于此配置文件。目前需要此项,因为 SDK 不会在运行时从已发布的配置文件清单中推导此列表。每个配置文件中的表填写一个逻辑名称(即下方每个 tables[] 条目的 logicalName 字段)。
appConfig.instanceUrlOFFLINE-CONFIG-WORKAROUND提供静态组织名称,使 SDK 无需在离线时访问 api.powerplatform.com 即可冷启动。其值与步骤 1 中记录的 <envUrl> 相同(例如 https://orgXXX.crm.dynamics.com)。此前此值写入 power.config.json.databaseReferences[*].databaseDetails.linkedEnvironmentMetadata.instanceUrl;运行时现在改为从此处的 appConfig.instanceUrl 读取。在 plugins/mobile-apps/template 中跟踪此上游改动。

请勿通过此技能修改 power.config.json。 早期草稿曾将相同字段写入 power.config.json.offline.* 和 databaseReferences[*].databaseDetails.linkedEnvironmentMetadata.instanceUrl。后来撤销了这些改动,因为 power.config.json 由 pa app init 生成,而其架构不受我们控制——在其中添加自定义字段意味着重新初始化可能会清除这些字段,上游架构变更也可能导致此技能失效。如果发现此处仍有代码路径会修改 power.config.json,请将其视为缺陷。

9c — 更新 memory-bank.md 中的 ## Offline profile:

status: done
profileId: <guid>
profileName: <name>
mode: create-new | extend
publishedOn: <iso8601>
gate1: approved
gate2: approved
gate3: approved
tablesCount: <N>
associationsCount: <M>

9d — 在 native-app-plan.md 中追加 ## Offline Profile 章节,总结最终状态(遵循数据模型章节的模式)。

步骤 9.5 — 验证

遥测检查点:verify_published_offline_profile

开始前打印:

“→ 正在验证服务器上的配置文件是否与 offline-profile.json 匹配……”

cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
node "${PLUGIN_ROOT}/scripts/verify-offline-profile.js" <envUrl> \
  --project-root '<working_dir>' --tenant-id "<tenantId>"

读取 JSON 输出:

status操作
ok所有检查均已通过。继续执行步骤 10。
drift向用户展示 drift[] 数组。全新创建后最常见的偏差是 unpublished(发布竞争);请重新运行 §9 发布并再次验证。其他偏差类型表示严重缺陷;请勿默默掩盖——返回 DONE_WITH_CONCERNS,并列出每一条偏差。
missing未写入 offline-profile.json。步骤 9a 静默失败——停止并请用户重新运行。
error身份验证或网络错误——向用户说明并停止。

验证脚本也是 /preview-offline-scope(v0.2)背后的标准实现——将其保留为此处的一个步骤,可确保每次全新创建时都会运行该脚本。

步骤 10 — 总结

打印:

✓ Mobile Offline Profile created.

  Profile name : <name>
  Profile ID   : <guid>
  Tables       : <N> (<M> required prereq enablement)
  Relationships: <K> associations
  Published    : <iso8601>

Saved to project:
  - offline-profile.json  (profile metadata + appConfig + tables)
  - memory-bank.md        (## Offline profile section)
  - native-app-plan.md    (## Offline Profile section)

Note: power.config.json was NOT modified (owned by pa app init).

Next steps:
  - /assign-offline-profile        → assign users / teams to this profile (not yet implemented — v0.3)
  - /preview-offline-scope         → estimate download size before pushing the app (not yet implemented — v0.2)
  - /edit-offline-profile <table>  → re-scope one table (not yet implemented — v0.2)

Runtime: The template bundles @microsoft/power-apps-native-offline.
@microsoft/power-apps-native-host consumes the profile and owns local SQLite access,
queued synchronization, reconnect behavior, and status UX. This skill configures
that runtime; it does not scaffold a second app-owned store, queue, or sync engine.

状态代码(最后一行)

  • DONE — 配置文件已创建并发布,产物已写入
  • DONE_WITH_CONCERNS: <list> — 已创建但存在注意事项(跳过验证、部分列已标记、部分表无法自定义)
  • NEEDS_CONTEXT: <missing> — 缺少数据模型、无法解析环境 URL 等
  • BLOCKED: <reason> — 身份验证失败、PrivilegeCheckFailed、环境 URL 无法访问,或子技能 /enable-tables-offline 返回 BLOCKED

故障恢复

每个步骤都会将检查点写入 memory-bank.md。流程中途失败时:

失败位置恢复方法
Gate 1 / Step 4重新运行 /setup-offline-profile — Step 1b 会启动恢复流程
Step 5软删除未完成的配置文件,从 Step 5 重试
Step 6(第 N 项,共 M 项)从第 N+1 项继续;之前的项目已提交
Step 7(关联)继续执行 — 关联为增量添加,重新 POST 已有配对会返回 409(忽略)
Step 8(发布)重新运行发布;配置文件不发布也能正常使用,但不会显示在 maker portal 中

切勿在会话结束后仍将状态为 status: in-progress 的未完成配置文件留在 memory-bank.md 中 — 下一会话的 Step 1b 会提示继续或删除该配置文件。

评论

支持 Markdown