关于 assign-offline-profile
assign-offline-profile 用于将现有 Mobile Offline Profile 分配给用户和/或团队,适合配置文件已创建但尚未绑定成员的情况。它先发现现有成员关系,再确认变更并执行添加或移除,最后验证结果。
主要功能
- 01按 ID 或名称定位离线配置文件
- 02选择用户和团队作为成员
- 03发现已有成员并计算变更差异
- 04添加或移除成员并验证结果
适用场景
- 01将离线配置文件分配给当前用户
- 02为指定用户或团队绑定离线配置文件
- 03从配置文件中移除已有用户或团队
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ microsoft/ power-platform-skills --skill assign-offline-profile安装后在 Claude Code 中输入/assign-offline-profile确认已加载。
详细介绍
共享说明:shared-instructions.md — 请先阅读。
参考资料:
- offline-profile-schema.md —
usermobileofflineprofilemembership/teammobileofflineprofilemembership实体字段映射 - dataverse-offline-api.md — Web API 操作指南(§12 — membership POST)
分配脱机配置文件
将一个或多个用户和/或团队绑定到现有的 Mobile Offline Profile。若不执行此步骤,该配置文件虽然存在于 Dataverse 中,但仍未绑定——实际上没有任何人的应用会将其用于脱机同步。
按照 maker portal 的 UX(环境设置下的“Assign profile to user”对话框),这是与创建配置文件分开的操作。很多用户会遇到“我创建了配置文件,但脱机功能仍无法使用”的问题——缺少的正是成员关系。
读取项目或执行命令前,请应用
app-working-directory.md。
为直接调用解析出一个绝对路径的 working_dir,或要求子调用指定其所属根目录。
确保所有 shell/文件操作、引用的操作指南
和重试都使用该根目录以及选定的环境/租户;缺少或冲突的上下文均为 NEEDS_CONTEXT。文件工具使用 <working_dir>/...。
规划阶段的调用可以发现并提出成员关系差异,但必须在确认关卡、POST/DELETE 或更新工件之前返回。
工作流
- 验证项目 + 定位配置文件 → 2. 选择用户/团队 → 3. 发现现有成员关系 → 4. 确认差异(单一关卡)→ 5. POST 成员关系 → 6. 验证 → 7. 总结
步骤 1 — 验证项目 + 定位配置文件
cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
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
捕获 <envUrl> 和 <tenantId>,并要求它们与任何提供的所属身份信息匹配。每次 Dataverse 请求都传入 --tenant-id "<tenantId>",包括配置文件查找、WhoAmI、成员关系发现、变更、回读和重试。
配置文件 ID 解析顺序:
| 来源 | 使用条件 |
|---|---|
$ARGUMENTS 包含 --profile-id <guid> | 显式覆盖 |
$ARGUMENTS 包含 --profile-name <name> | 通过 GET /mobileofflineprofiles?$filter=name eq '<name>'&$select=mobileofflineprofileid 解析 |
<working_dir>/offline-profile.json | 读取顶层 profileId 字段 |
| 否则 | 执行 GET /mobileofflineprofiles 并使用列表呈现 AskUserQuestion(最多 4 个选项) |
若无法解析配置文件,立即停止。打印:Run /setup-offline-profile first, or pass --profile-id。
这里有意不读取
power.config.json。 该文件由pa app init管理。配置文件 ID 只保存在offline-profile.json中。
步骤 2 — 选择用户/团队
$ARGUMENTS 解析规则:
| 标志 | 效果 |
|---|---|
--user <upn>(可重复) | 按 UPN([email protected])添加指定用户 |
--team <name>(可重复) | 按名称添加指定团队 |
--me | 通过 WhoAmI / systemusers(<UserId>) 添加当前 Dataverse 用户——适用于个人开发演示 |
--all-app-users | 添加当前环境中所有具有 System User 角色的用户(范围较广;适用于生产环境发布——在确认关卡确认) |
--unassign-user <upn> / --unassign-team <name> | 移除现有成员关系,而不是添加 |
如果未传入任何标志,请呈现 AskUserQuestion:
问题:“谁应该使用此脱机配置文件?”
选项(最多 4 个):
Just me (the current user)— 等同于--mePick specific users by UPN— 请在下一条消息中用逗号分隔填写电子邮件地址Pick a team— 列出环境中的团队并选择一个All users with System User role— 等同于--all-app-users;范围较广,需在确认关卡确认
对于选择用户的流程:收到选择后,打印:
“请回复以逗号分隔的 UPN(例如
[email protected], [email protected])”
然后读取下一条用户消息并进行解析。
步骤 3 — 发现现有成员关系
遥测检查点:discover_offline_profile_memberships
为确保幂等性:
cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
# Existing user memberships for this profile
node "${PLUGIN_ROOT}/scripts/dataverse-request.js" <envUrl> GET \
"usermobileofflineprofilememberships?\$filter=_mobileofflineprofileid_value eq <profileId>&\$select=usermobileofflineprofilemembershipid,_systemuserid_value&\$expand=systemuserid_systemuser(\$select=domainname)" \
--tenant-id "<tenantId>"
# Existing team memberships for this profile
node "${PLUGIN_ROOT}/scripts/dataverse-request.js" <envUrl> GET \
"teammobileofflineprofilememberships?\$filter=_mobileofflineprofileid_value eq <profileId>&\$select=teammobileofflineprofilemembershipid,_teamid_value&\$expand=teamid_team(\$select=name)" \
--tenant-id "<tenantId>"
构建 already-bound UPN 和团队名称的集合。
对于步骤 2 中的每个候选用户/团队,查询其 systemuserid/teamid(如果已在 already-bound 中,则跳过):
cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
node "${PLUGIN_ROOT}/scripts/dataverse-request.js" <envUrl> GET \
"systemusers?\$filter=domainname eq '<upn>'&\$select=systemuserid,fullname,domainname&\$top=1" \
--tenant-id "<tenantId>"
node "${PLUGIN_ROOT}/scripts/dataverse-request.js" <envUrl> GET \
"teams?\$filter=name eq '<team-name>' and teamtype eq 0&\$select=teamid,name&\$top=1" --tenant-id "<tenantId>"
(teamtype eq 0 会排除 Access Teams 和 Owner Teams,只有 Manage Teams 会获得配置文件分配。)
构建三个列表:
to_add— 要 POST 的已解析 IDto_remove— 要 DELETE 的已解析 ID(来自--unassign-*标志)not_found— 未解析到的 UPN/团队名称(发出警告)already_bound— 已跳过的空操作
步骤 4 — 确认差异(单一关卡)
遥测检查点:confirm_offline_profile_assignment_diff
AskUserQuestion:
问题标题:
Confirm membership changes问题正文:
Profile: <name> (<profileId>) Will ADD: - User: [email protected] (Rahul Bansal) - User: charanma@... (Charan Mahankali) - Team: Field Service RMs (12 members) Will REMOVE: (none) Already bound (skipping): - User: admin@... (no-op) Could not resolve: - [email protected] — not in this env's system users Proceed?选项:
ProceedCancel
步骤 5 — POST 成员关系
遥测检查点:assign_offline_profile_memberships
对于 to_add 中的每一项,按顺序 POST(并行 POST 偶尔会返回 429):
用户成员关系:
cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
node "${PLUGIN_ROOT}/scripts/dataverse-request.js" <envUrl> POST \
"usermobileofflineprofilememberships" \
--tenant-id "<tenantId>" \
--body '{
"[email protected]": "/mobileofflineprofiles(<profileId>)",
"[email protected]": "/systemusers(<systemuserid>)"
}' \
--include-headers
预期返回 204 和 OData-EntityId → 捕获成员关系 GUID。
团队成员关系:
cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
node "${PLUGIN_ROOT}/scripts/dataverse-request.js" <envUrl> POST \
"teammobileofflineprofilememberships" \
--tenant-id "<tenantId>" \
--body '{
"[email protected]": "/mobileofflineprofiles(<profileId>)",
"[email protected]": "/teams(<teamid>)"
}' \
--include-headers
对于 to_remove 中的每一项,执行 DELETE:
cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
node "${PLUGIN_ROOT}/scripts/dataverse-request.js" <envUrl> DELETE \
"usermobileofflineprofilememberships(<membershipid>)" --tenant-id "<tenantId>"
⚠️ 重复项处理: POST 已存在的成员关系会返回
409 Conflict。dataverse-request.js包装器的looksLikeDuplicate补救逻辑会将其视为静默成功(步骤 3 的去重应能先捕获大多数情况)。重复运行是安全的。
步骤 6 — 验证
遥测检查点:verify_offline_profile_memberships
重新查询步骤 3 中的成员关系,并断言差异已应用:
- 每个
to_add现在都出现在 GET 响应中 - 每个
to_remove都不再出现在响应中
如果验证结果不一致,则返回 BLOCKED: membership writes did not commit,并打印差异。
步骤 7 — 汇总
打印:
✓ Membership updates applied.
Profile : <name>
Total members: <N users + M teams>
Added : <list>
Removed : <list>
Skipped : <list> (already bound)
Users will receive the profile on their next mobile app sign-in. Existing
sessions need to sign out + sign in to trigger the profile pull.
更新 memory-bank.md 中的 ## Offline profile 区块:
membership:
users: [rahul@..., charanma@...]
teams: [Field Service RMs]
lastAssignedAt: 2026-05-19T...
状态代码(最后一行)
DONE— 每项请求的添加/移除均已应用;验证已确认DONE_WITH_CONCERNS: <list>— 部分 UPN/团队无法解析,或--all-app-users匹配到 0 个用户(环境可能尚未授予该角色)NEEDS_CONTEXT: <missing>— 无法确定 profileId(没有 offline-profile.json、没有 --profile 标志,或环境中没有配置文件)BLOCKED: <reason>— 身份验证失败、环境中未找到配置文件,或验证结果不一致
故障恢复
成员关系会逐项提交(没有事务)。如果步骤 5 在循环中途失败:
- 已部分添加的成员关系会保留(在环境中可见)
- 使用相同参数重新运行是幂等的(步骤 3 的去重会捕获已绑定的成员关系)
- 如有需要,可使用
--unassign-*撤销特定绑定