关于 setup-offline-profile
用于在 Power Apps 项目中配置 Dataverse Mobile Offline Profile 的工作流技能。它支持创建或扩展配置,检查表范围与关系,并为原生离线应用保存设置。
主要功能
- 01创建或扩展离线配置
- 02设置各表行范围
- 03配置列、关系与同步
- 04验证并发布配置
- 05保存设置并支持续跑
适用场景
- 01为原生 Power Apps 应用设置离线数据
- 02扩展现有 Dataverse 离线配置
- 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 — 请先阅读。
参考资料:
- offline-profile-schema.md — Dataverse 实体字段映射
- dataverse-offline-api.md — 用于 profile / item / association POST 请求的 Web API 示例
- offline-profile-reconciliation.md — 此技能写入的
schemaColumns基准,以及使用该基准的生命周期差异检查
设置离线配置文件
用于创建 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。
工作流
- 验证项目和身份验证 → 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 且配置文件仍然存在时:
-
执行
GET /mobileofflineprofiles(<profileId>)?$expand=MobileOfflineProfile_MobileOfflineProfileItem,计算实际已提交的内容:- 0 个项 → 配置文件外壳已创建,但尚未 POST 项。从步骤 6 恢复。
- 1 个或更多项,但清单中仍有缺失项 → 从步骤 6 恢复,跳过已存在的项。
- 所有项均已存在,但至少 1 项的
selectedcolumns为空 → 从步骤 7(PATCH)恢复。 - 所有项均已存在,且每项都有
selectedcolumns→ 从步骤 8(发布)恢复。 componentstate=0(已发布)→ memory-bank 记录有误;视为done。
-
向用户提出一个合并的 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 -
选择
Resume→ 跳转至计算出的步骤。通过匹配selectedentitytypecode跳过已提交的项。 -
选择
Start fresh→ 执行DELETE /mobileofflineprofiles(<profileId>)(级联删除项和关联),清除 memory-bank 区块,然后作为新运行继续执行步骤 2。 -
选择
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 中处理)。此处不要重新评估。 |
| 2 | offline-profile.json 的顶层包含 profileId: <X>,且环境中仍存在配置文件 <X> | 冲突情况 — 询问用户。 AskUserQuestion:(a)扩展固定的配置文件(根据当前数据模型重新设计,并按需添加/PATCH 项目);(b)删除固定的配置文件并重新创建(不可撤销 — 级联删除项目、关联和成员关系);(c)取消。默认 =(a)扩展。绝不默默删除。 |
| 3 | offline-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 → orderline1: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 关系):
- 通过
EntityDefinitions(LogicalName='<PARENT-table>')/OneToManyRelationships,筛选SchemaName === <relationshipSchemaName>,解析relationshipid(MetadataId GUID)。按父表缓存查找结果。 - POST 该关联(依次执行——并行 POST 偶尔会返回 412 PreconditionFailed)。
- 从
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,但没有关联指向它。从错误消息中解析表名,并向用户提供两个选项:
/add-table-to-offline-profile --table <name> --add-associations— 重新运行架构设计器以发现关系,并 POST 缺失的关联。/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 调用返回后的处理流程:
-
如果状态为
204→ 发布成功,继续。 -
如果状态为
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。
-
如果状态不是
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(该关系的MetadataIdGUID——这是规范比较键,在服务器端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");
'
字段说明:
| 字段 | 层级 | 原因 |
|---|---|---|
profileId | A(真实字段) | 来自 Dataverse 的已发布 mobileofflineprofileid——运行时会获取此配置文件的清单。offline-profile.json 是此值的唯一事实来源。 |
appConfig.enabled | A(真实字段) | 将项目标记为已启用离线功能;由运行时在启动时使用。 |
appConfig.serverRowLimit | A(真实字段) | 对应图 2 的“数据行限制”字段(默认值为 2000)。在 v0 中,此值仅用于配置——运行时强制执行属于 B 层的工作。 |
appConfig.useDda | OFFLINE-CONFIG-WORKAROUND | 强制 SDK 绕过有问题的 shouldUseDda 门控(实体集与逻辑名称不匹配)。当 @microsoft/react-native-dataverse-offline 中的该门控完成协调后移除此项(在 PowerApps-Client 中跟踪)。 |
appConfig.entitiesIncluded | OFFLINE-CONFIG-WORKAROUND | SDK 的离线引导流程会读取此列表,以确定哪些实体逻辑名称属于此配置文件。目前需要此项,因为 SDK 不会在运行时从已发布的配置文件清单中推导此列表。每个配置文件中的表填写一个逻辑名称(即下方每个 tables[] 条目的 logicalName 字段)。 |
appConfig.instanceUrl | OFFLINE-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 会提示继续或删除该配置文件。