关于 add-native
用于为 Power Apps Wrap 移动应用添加受支持原生功能的技能。它会检查已安装的包,并将特定流程交由专用辅助技能处理;其他符合条件的功能则生成类型化封装。
主要功能
- 01检查原生包及运行时限制
- 02在 src/native/ 生成类型化封装
- 03提供相机、PDF、笔迹和地理位置专用流程
- 04说明 Dataverse 文件和图像列的宿主控件用法
适用场景
- 01为移动应用添加符合条件的原生功能
- 02实现相机、PDF 或签名流程
- 03正确处理 Dataverse 文件或图像字段
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ microsoft/ power-platform-skills --skill add-native安装后在 Claude Code 中输入/add-native确认已加载。
详细介绍
插件检查:运行
node "${PLUGIN_ROOT}/scripts/check-version.js"—— 如果输出了消息,请在继续之前将其显示给用户。
📋 共享说明:shared-instructions.md —— 请先阅读。
工作目录: 在读取任何项目内容或运行任何命令之前,执行
native-artifact-compatibility.md 第 0 步。
在此处解析 --working-dir / 所有者上下文;对每次 shell 调用和文件工具调用,都使用解析出的绝对根目录,而不要使用之前某个 shell 的当前工作目录。
添加原生功能
入口路由: 请先按照下文工作流中的要求,使用共享的 App 功能入口点 进行预检。
此叶级流程会在 src/native/ 下为上游模板已提供的原生功能生成类型化封装。屏幕应导入封装,而不是直接调用 Expo 模块,从而确保判别联合结果契约保持一致。
严格规则 — 不得违反
- 绝不要为原生模块运行
npx expo install、npm install或yarn add。package.json中的原生模块集合由上游模板固定。添加新模块会破坏重新封装流程(客户的二进制文件是基于预构建的基础版本构建的,而不是根据其package.json构建的)。 - 绝不要编辑
app.config.js—— 包括插件、ios.infoPlist、android.permissions或其他任何内容。模板提供的所有原生配置都有其设计意图并已获批准;在重新封装时无法接受任意添加的配置。 - 绝不要修改
package.json中的原生模块dependencies。 原生模块是指包含平台源代码/项目、podspec、codegen、Expo 模块/配置插件或react-native.config.js的包;仅凭包名的前缀无法证明它是原生模块。纯 JavaScript 依赖不属于/add-native的范围,应由/create-mobile-app或/edit-app根据已批准的JavaScript Dependencies计划安装。 - 如果
package.json中实际上没有所请求的模块 — 停止。 这意味着上游模板尚未提供该模块;不要通过安装来绕过限制。
路由 — /add-native 是公开入口
某些功能有专用的实现辅助工具,能够完成超出普通封装的工作(camera 会写入扫描器/上传辅助逻辑;PDF 报告/查看器辅助工具会强制执行本地与 HTTPS 边界;pen 会进行针对原生控件的验证)。用户仍应对原生控件调用 /add-native <capability>。如果存在专用实现,请在内部运行,不要让用户直接运行该辅助工具。
**查找约定:**规范化功能名称后,首先检查下方的内部辅助工具映射。对于 camera、image-picker、barcode-scanner 和 qr-scanner,在此次 /add-native 调用中读取并执行 ${PLUGIN_ROOT}/skills/add-native/add-camera/SKILL.md。对于 pdf-report,读取并执行 ${PLUGIN_ROOT}/skills/add-native/add-pdf-report/SKILL.md。对于 pdf-viewer,读取并执行 ${PLUGIN_ROOT}/skills/add-native/add-pdf-viewer/SKILL.md。对于 pen-input,读取并执行 ${PLUGIN_ROOT}/skills/add-native/add-pen-input/SKILL.md。对于 geolocation,读取并执行 ${PLUGIN_ROOT}/skills/add-native/add-geolocation/SKILL.md。如果没有辅助工具,则继续执行本技能的内联封装流程。
当前专用实现:
| 功能 | 专用技能 | 专用原因 |
|---|---|---|
camera、take-photo、photo、expo-camera、image-picker、gallery、expo-image-picker、barcode-scanner、qr-scanner、scanner | add-camera 内部辅助工具 | 负责照片拍摄、图库图片选择,以及由 expo-camera / expo-image-picker 支持的条码/QR 扫描器控件 |
pdf-report、pdf-export、generate-pdf、print-report、evidence-packet | add-pdf-report 内部辅助工具 | 使用 expo-print 生成应用自有的本地 PDF,并且仅在存在 expo-sharing 时共享 |
pdf-viewer、native-pdf-viewer、pdf-control、open-pdf、@microsoft/power-apps-native-pdf-viewer | add-pdf-viewer 内部辅助工具 | 强制要求查看器输入为 https:// / file://,并处理原生查看器结果 |
pen-input、signature、ink、draw、@microsoft/power-apps-native-pen-input | add-pen-input 内部辅助工具 | 捕获 PNG 数据 URI,并说明 Dataverse Image/File 持久化 |
geolocation、location-tracking、background-location、gps-tracking、geo-tracking、@microsoft/power-apps-native-bglocation | add-geolocation 内部辅助工具 | 原生后台 GPS 跟踪,支持持久化存储和内联 Dataverse 同步;不同于一次性 expo-location |
对于下方列出的其他所有功能,本技能会直接编写封装。
原生功能准入门槛
在添加任何原生控件或封装之前,必须通过所有准入门槛:对意图进行分类,根据实时的 package.json 确定确切的包或控件,确认其未被运行时禁用,确认输入、输出和存储限制,然后使用下方对应的路径。如果任何一项未通过,则此应用版本不支持该原生功能——不要安装包、编辑原生配置或创建虚假封装。
| 用户意图 | 添加/使用 | 所需包或控件 | 不要使用/回退方案 |
|---|---|---|---|
| 绑定到 Dataverse File 列的表单字段 | 在屏幕 JSX 中托管 <FilePicker> | @microsoft/power-apps-native-host 托管控件 | 不要为此字段生成文档选择器/文件系统/共享封装 |
| 绑定到 Dataverse Image 列的表单字段 | 在屏幕 JSX 中托管 <ImagePicker> | @microsoft/power-apps-native-host 托管控件 | 对常规表单绑定的图像字段,不要使用相机/图像选择器封装 |
| 专用照片/图库/扫描器工作流 | /add-native camera、image-picker 或 barcode-scanner | 照片/图库使用 expo-image-picker;扫描器使用 expo-camera;组合请求需同时使用两者 | 如果缺少相关包,请停止并说明所需包 |
| 选择/导入/上传用户选取的 PDF/文档 | /add-native document-picker;Dataverse File 字段使用托管的 <FilePicker> | 已安装 expo-document-picker,或使用托管的 File 控件 | 不要将此功能当作 pdf-report 或原生 PDF 查看器 |
| 生成/导出/打印应用自有的报告 PDF | /add-native pdf-report | 已安装 expo-print | 如果未安装 expo-print,不要添加 PDF 报告功能 |
| 共享已生成的本地 PDF | pdfReport.ts 共享辅助程序 | 已安装 expo-sharing | 如果未安装 expo-sharing,不要呈现共享界面 |
| 打开/预览 HTTPS 或本地文件 PDF | /add-native pdf-viewer | 已安装 @microsoft/power-apps-native-pdf-viewer 0.2.9+,且输入为 https:// 或 file:// | 不要将 content://、blob: 或 http:// URI 传给查看器 |
| 捕获签名、墨迹、绘图或签字确认 | /add-native pen-input | 已安装 @microsoft/power-apps-native-pen-input | 如果需要持久化,请先规划 Dataverse Image/File/子 Evidence 存储目标 |
| 持续/后台 GPS 跟踪并可靠上传到 Dataverse | /add-native geolocation | 已安装 @microsoft/power-apps-native-bglocation | 后台跟踪不要使用一次性的 expo-location;不要使用 GeolocationExtension/HostingSDK 路径 |
| 按压、选择和操作结果的触觉反馈 | /add-native haptics | 已安装 expo-haptics | 使用类型化封装,并为每次触觉反馈配套可见的 UI 反馈 |
| 存储生成的 PDF/签名文件 | 父行存在后使用生成的 Dataverse 服务 | 存在 File/Image 列或子 Evidence/Attachment 表 | 绝不要将 File 字节放入创建/更新 JSON |
| 本表未列出的原生功能 | 先根据 package.json 确定,然后仅在存在对应包且未被运行时禁用时添加内联封装 | package.json 中存在确切的相关包 | 如果不存在相关包,或该包被运行时禁用,请添加说明情况的提示并停止 |
对多部分请求逐行处理。例如:“捕获签名并附上已签名的报告 PDF”需要 pen-input、pdf-report、Dataverse 文件存储,可能还需要 sharing;不要只添加原生功能,却不定义存储或屏幕状态。本表并非封闭清单:对于新发布的包,应根据功能语义确定用途,在安全的前提下使用直接匹配的包;仅当多个已安装的包都可能匹配时,才询问一次。
文件/图像选择器归属
此技能负责原生文件能力机制。屏幕构建器只负责最终的 JSX 放置位置。
有两种不同的情况:
| 情况 | 正确实现 | 归属 |
|---|---|---|
Dataverse File 列 | 使用 @microsoft/power-apps-native-host 中的 <FilePicker> | 屏幕构建器 JSX 规则,本文档对此有说明 |
Dataverse Image 列 | 使用 @microsoft/power-apps-native-host 中的 <ImagePicker> | 屏幕构建器 JSX 规则,本文档对此有说明 |
| 未绑定到 Dataverse 列的本地设备文档/文件工作流 | 生成/使用 src/native/documentPicker.ts、src/native/fileSystem.ts 和/或 src/native/sharing.ts 包装器 | /add-native |
| 应用生成的 PDF 报告工作流 | /add-native 在内部路由到 add-pdf-report;仅在存在时使用 expo-print,并可选使用 expo-sharing | /add-native |
| 相机拍摄、图库图像选择、条码扫描器或 QR 扫描器工作流 | /add-native 在内部路由到 add-camera | /add-native |
Dataverse File/Image 列使用宿主控件,而非原始 Expo 模块或应用专用原生包装器。 当表单字段绑定到 Dataverse File 列(FileAttributeMetadata)时,请使用 @microsoft/power-apps-native-host 中的 <FilePicker>。当字段绑定到 Dataverse Image 列(ImageAttributeMetadata)时,请使用 @microsoft/power-apps-native-host 中的 <ImagePicker>。这些控件会从 PowerAppsProvider / ThemeProvider 读取强调色、表面颜色和文本颜色,并生成兼容 Dataverse 的有效负载。
import { FilePicker, ImagePicker } from '@microsoft/power-apps-native-host';
import type { PickedFileInfo, PickedImageInfo } from '@microsoft/power-apps-native-host';
// Image column — seed preview from Dataverse bytes/base64 and capture upload-ready payload.
<ImagePicker
label="Site Photo"
initialBase64={imageBase64}
onImageChange={(image: PickedImageInfo | null) => { /* store image for upload */ }}
/>
// File column — edit-only pick/replace.
<FilePicker
label="Specification Sheet"
fileName={record.cr123_specsheet_name}
onChange={(file: PickedFileInfo) => { /* store file for save */ }}
/>
// File column — view + replace.
<FilePicker
label="Specification Sheet"
fileName={record.cr123_specsheet_name}
onDownload={() => Cr123_inspectionService.downloadFile(id, 'cr123_specsheet')}
onChange={(file: PickedFileInfo) => { /* store file for save */ }}
/>
宿主选择器绑定的 Dataverse 列的保存/读取模式:
// FILE/IMAGE columns: do not include picker bytes in the PATCH body.
// Save normal fields first, then upload through generated service helpers.
await Cr123_inspectionService.update(id, { cr123_name: name.trim() });
await Cr123_inspectionService.upload(id, 'cr123_specsheet', file.file, file.name);
await Cr123_inspectionService.upload(id, 'cr123_sitephoto', image.file, image.name);
// Download helpers are used for read/view scenarios.
const fileBytes = await Cr123_inspectionService.downloadFile(id, 'cr123_specsheet');
const imageBytes = await Cr123_inspectionService.downloadImage(id, 'cr123_sitephoto');
使用宿主 ImagePicker / FilePicker,并通过生成的 upload(...) 进行持久化,通过 downloadFile(...) / downloadImage(...) 进行读取/查看。请勿使用 Service.update(...{ <imageColumn>: base64 }) 持久化由选择器驱动的图像/文件。
请勿使用原始的 expo-document-picker、expo-image-picker、expo-file-system 或 expo-sharing 为 Dataverse File/Image 列构建自定义 UI。宿主 FilePicker 和 ImagePicker 已处理权限、读取、共享/下载功能以及兼容 Dataverse 的有效负载格式。
仅当工作流不涉及 Dataverse File/Image 列时,才使用 /add-native 包装器,例如导入本地 PDF 进行离线解析、导出生成的报告、保存本地草稿材料包,或打开共享面板以分享应用生成的摘要。
支持的能力
应用上述原生能力门控。此表是已知的能力与软件包映射,并不保证每个列出的软件包都存在于每个模板版本中。
| 能力 | 模块 | 要生成的封装 | 备注 |
|---|---|---|---|
camera, take-photo, photo, expo-camera | expo-image-picker | src/native/camera.ts | /add-native 会在内部将 photo 工件路由到 add-camera;expo-camera 是能力别名,而非必需的软件包 |
image-picker, gallery, expo-image-picker | expo-image-picker | src/native/camera.ts (pickImage) | /add-native 会在内部路由到 add-camera;保留兼容的现有重新导出项 |
barcode-scanner, qr-scanner, scanner, barcode, qr | expo-camera | src/native/barcodeScanner.tsx | /add-native 会在内部路由到 add-camera |
document-picker | expo-document-picker | src/native/documentPicker.ts | 从设备中选择/导入用户选定的文件(PDF、文档等) |
pdf-viewer, native-pdf-viewer, pdf-control, open-pdf, @microsoft/power-apps-native-pdf-viewer | @microsoft/power-apps-native-pdf-viewer | src/native/pdfViewer.ts | /add-native 会在内部路由到 add-pdf-viewer;0.2.9+ 支持打开 HTTPS URL 和文件 URI |
pdf-report, pdf-export, generate-pdf, print-report, evidence-packet | expo-print(+ 可选的 expo-sharing) | src/native/pdfReport.ts | /add-native 会在内部路由到 add-pdf-report;仅当存在 expo-sharing 时才会共享生成的本地文件,否则会将其上传到 Dataverse |
pen-input, signature, ink, draw, @microsoft/power-apps-native-pen-input | @microsoft/power-apps-native-pen-input | src/native/penInput.ts | /add-native 会在内部路由到 add-pen-input;捕获 PNG 数据 URI |
geolocation, location-tracking, background-location, gps-tracking, geo-tracking, @microsoft/power-apps-native-bglocation | @microsoft/power-apps-native-bglocation | src/native/geolocation.ts | /add-native 会在内部路由到 add-geolocation;原生后台跟踪 + 持久化 Dataverse 同步。与下方的一次性 location 不同 |
secure-store | expo-secure-store | src/native/secureStore.ts | |
file-system | expo-file-system | src/native/fileSystem.ts | |
sharing | expo-sharing | src/native/sharing.ts | |
location | expo-location | src/native/location.ts | 仅用于一次性/前台定位。若要进行持续的后台跟踪并同步到 Dataverse,请使用 geolocation(@microsoft/power-apps-native-bglocation)。仅当当前模板包包含 expo-location 时使用 |
biometrics, local-authentication | expo-local-authentication | src/native/biometrics.ts | 仅当当前模板包包含 expo-local-authentication 时使用 |
clipboard | expo-clipboard | src/native/clipboard.ts | 仅当当前模板包包含 expo-clipboard 时使用 |
mail-composer, email-draft | expo-mail-composer | src/native/mailComposer.ts | 如果存在该软件包,则打开原生邮件撰写界面;服务器端邮件发送仍由连接器负责 |
audio | expo-audio | src/native/audio.ts | 仅当存在该软件包时用于音频录制/播放 |
video | expo-video | src/native/video.ts | 仅当存在该软件包时用于视频播放 |
sensors | expo-sensors | src/native/sensors.ts | 仅用于已安装软件包所提供的传感器 API |
screen-orientation | expo-screen-orientation | src/native/screenOrientation.ts | 仅当存在该软件包时使用;不要编辑原生配置 |
haptics, vibration-feedback, impact-feedback, selection-feedback, notification-feedback | expo-haptics | src/native/haptics.ts | 提供冲击、选择以及成功/警告/错误通知反馈 |
device-info | expo-device / expo-application / expo-cellular | src/native/deviceInfo.ts | 只读设备/应用/蜂窝网络元数据封装 |
date-time-picker | @react-native-community/datetimepicker | 屏幕级组件用法 | 根据屏幕构建器规则,直接在表单屏幕中使用;无需 /add-native 封装 |
对于 Dataverse 文件/图像表单字段之外的自定义工作流,用户选择照片和视频时,通过 /add-native image-picker 使用 image-picker 功能;选择文档和其他文件时,通过 /add-native document-picker 使用 document-picker 功能。这些封装使用受限的系统选择器流程。Dataverse 文件/图像表单字段仍是例外:应按上文所述使用宿主提供的 <FilePicker> / <ImagePicker> 控件,而不是 /add-native 封装或直接导入 Expo 模块。 |
PDF / 笔输入路由规则
- 不要将所有 PDF 请求都视为
document-picker。 - 用户要选择、导入或上传本地 PDF/文档时,使用
document-picker。此功能仍受支持,且在此用例中仍应使用。 - 应用根据记录、证据、检查数据、证书、收据或报告生成 PDF 时,使用
pdf-report,但前提是package.json中存在expo-print。 - 应用使用
@microsoft/power-apps-native-pdf-viewer0.2.9+ 打开/预览 HTTPS PDF URL 或本地file://URI 时,使用native-pdf-viewer/pdf-control。不支持content://、blob:和http://URI。 - 如果请求是“查看/打开 PDF”,但缺少 Power Apps 查看器包,则仅当应用要生成自己的报告且存在
expo-print时,才回退到pdf-report。不要声称expo-print支持通用 PDF 查看;它生成本地文件,并不查看任意 PDF。 - 仅在使用
@microsoft/power-apps-native-pen-input进行签名、绘制审批、墨迹笔记、草图和手写签收时,使用pen-input。 - 对于其他用例,使用
package.json中已有的相关 Expo 模块或其他依赖;不要在无关工作流中强行使用 Power Apps 扩展。 - 对于通过
expo-print生成的本地 PDF,使用 native PDF viewer 0.2.9+ 打开/预览,使用expo-sharing分享,并使用 Dataverse 文件存储进行保留。仅预览本地 PDF 时,不要要求使用expo-sharing。 - 对于用户选择的 Dataverse 文件/图像表单字段,宿主提供的
FilePicker和ImagePicker仍是正确选择。生成的 PDF 和笔输入捕获内容应优先使用原生封装,然后再使用 Dataverse 持久化辅助工具。
Dataverse 构件持久化规则
- 笔输入会返回类似
data:image/png;base64,...的 PNG 数据 URI。 - 对于 Dataverse 图像列,将数据 URI 规范化为生成的服务负载格式。如果需要原始 base64,请去掉
data:image/png;base64,前缀。 - 对于 Dataverse 文件列,将生成的 PDF 或签名 PNG 转换为上传字节/File,先保存或更新父行,然后通过生成的服务辅助工具上传。
- 切勿将文件列字节放入创建/更新 JSON 请求体。只有在父行 ID 存在后,才能上传文件字节。
- 屏幕必须明确处理不支持、已取消、上传失败和查看器失败等状态。笔输入取消属于非错误结果,屏幕可以忽略。
缺少或受限的包:package.json 和运行时门控是权威依据。如果缺少相关包/控件,请停止并说明情况。
工作流
- 验证项目 → 2. 确定功能 → 3. 如果存在专用技能,则自动路由至该技能 → 4. 验证模块是否随模板提供 → 5. 编写封装 → 6. 类型检查 → 7. 总结
步骤 1 — 验证项目
cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
test -f app.config.js && test -f power.config.json && test -f package.json || { echo "BLOCKED: working_dir is not an initialized app" >&2; exit 1; }
第 2 步 — 解析功能
遥测检查点:resolve_native_capability
如果 $ARGUMENTS 包含功能名称、包名称或控件名称,请使用该名称。否则,查找 native-app-plan.md 中的 ## Native Capabilities 部分,并列出计划中的功能供用户确认。如果两者都不存在,请向用户展示上文支持的功能列表,以及 package.json 中与其请求直接匹配的任何相关已安装包。
将功能名称规范化为小写、以连字符分隔的形式(例如,Camera → camera、ImagePicker → image-picker、SecureStore → secure-store)。同时规范化别名:take-photo / photo / camera-control / expo-camera → camera;gallery / pick-image / expo-image-picker → image-picker;scanner / barcode / qr → barcode-scanner;open-pdf / view-pdf / pdf-control / pdf-viewer-control / @microsoft/power-apps-native-pdf-viewer → pdf-viewer;native-pdf-viewer → pdf-viewer;generate-pdf / pdf-export → pdf-report;signature / sign-off / ink / draw / pen-control / @microsoft/power-apps-native-pen-input → pen-input;location-tracking / background-location / gps-tracking / geo-tracking / track-location / power-apps-native-bglocation / @microsoft/power-apps-native-bglocation → geolocation;vibration / vibration-feedback / impact-feedback / selection-feedback / notification-feedback / expo-haptics → haptics。
当用户提出“location”或“GPS”时,请根据意图消除歧义:持续/后台跟踪或上传到持久化的 Dataverse → geolocation(@microsoft/power-apps-native-bglocation);单次前台坐标读取 → location(expo-location)。如果意图不明确,请先询问一次,再进行路由。
如果用户指定的内容不在支持表格中,请应用 Native capability gate:从 package.json 中解析相关包,仅当该包存在且未被运行时禁止时才继续;否则,请停止并说明情况。
第 3 步 — 路由到嵌套辅助程序或内联封装
遥测检查点:dispatch_native_capability
对于规范化后的 camera、image-picker、barcode-scanner、qr-scanner、pdf-report、pdf-viewer、pen-input 或 geolocation,不要继续执行通用封装流程,也不要让用户运行另一个斜杠命令。读取嵌套辅助程序,并在本次 /add-native 调用中遵循其步骤:
cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
case "<capability>" in
camera|image-picker|barcode-scanner|qr-scanner) test -f "${PLUGIN_ROOT}/skills/add-native/add-camera/SKILL.md" && echo "INTERNAL_HELPER:add-camera" ;;
pdf-report) test -f "${PLUGIN_ROOT}/skills/add-native/add-pdf-report/SKILL.md" && echo "INTERNAL_HELPER:add-pdf-report" ;;
pdf-viewer) test -f "${PLUGIN_ROOT}/skills/add-native/add-pdf-viewer/SKILL.md" && echo "INTERNAL_HELPER:add-pdf-viewer" ;;
pen-input) test -f "${PLUGIN_ROOT}/skills/add-native/add-pen-input/SKILL.md" && echo "INTERNAL_HELPER:add-pen-input" ;;
geolocation) test -f "${PLUGIN_ROOT}/skills/add-native/add-geolocation/SKILL.md" && echo "INTERNAL_HELPER:add-geolocation" ;;
*) echo "INLINE" ;;
esac
- **INTERNAL_HELPER:**读取打印出的辅助程序文件,并使用相同的绝对
working_dir、参数、用户提供的回答、模式标志以及当前的所有者/阶段/approved_scope执行其工作流。即使从其他目录启动,每个辅助程序也必须通过步骤 0 将所有操作绑定到该根目录。每个辅助程序都必须在写入/复用之前以及成功摘要之前执行 native-artifact-compatibility.md;此分支不会到达外层步骤 5。将NEEDS_CONTEXT/BLOCKED和制品结果传递给所有者,然后停止当前叶级流程,而非停止所属工作流。不要继续执行内联生成,也不要将辅助程序的不完整结果报告为成功。/add-native仍是这些控件唯一面向用户的命令。 - **INLINE:**继续执行步骤 4。
第 4 步 — 验证模块是否随模板提供
确认底层原生功能包确实存在于项目的 package.json 中(以捕获用户手动移除该包,或模板版本早于预期的情况):
cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
node -e "const p = require('./package.json'); const m = '<expo-module-name>'; if (!p.dependencies?.[m]) { console.error('MISSING: ' + m + ' is not in package.json. The template should ship it. Re-scaffold via /create-mobile-app, restore it from upstream, or wait for the template release that adds it — this skill will not install it.'); process.exit(1); }"
如果检查失败,请停止。不要运行 npx expo install。逐字打印错误信息。
第 5 步 — 编写封装
遥测检查点:generate_native_wrapper
开始前打印:
"→ 正在编写 src/native/.ts(带判别联合结果和 iOS/Android 平台保护的类型化封装)…"
读取并执行 native-artifact-compatibility.md
中针对内联能力行的步骤 1–3,然后再编写或复用输出文件。
仅当 src/native/<wrapper-filename>.ts 文件不存在且属于范围内时才创建。如果
文件已存在,请检查其导出是否符合已批准的能力契约,包括行为和存储。兼容时原样
复用;若已获批准,则无需再次询问,进行范围内更新。否则,在更新前向负责人返回
NEEDS_CONTEXT(或在独立场景中询问);绝不能悄悄跳过该功能或覆盖自定义代码。
每个封装导出:
request<Capability>Permission(): Promise<boolean>— 仅当底层 API 具有权限模型时提供(位置、生物识别、通知)。不适用的能力(安全存储、文件系统、分享、剪贴板)请跳过。- 一个或两个返回类型化结果的领域方法。
screen-builder Agent 所依赖的契约:
- 所有封装都返回判别联合结果(
{ ok: true, ... } | { ok: false, reason, message? })——绝不抛出异常 - 不支持的运行时/平台状态应优雅降级,或返回
{ ok: false, reason: 'unsupported' }——绝不崩溃 - 当某项能力在 iOS 和 Android 上存在差异时,按受支持的原生平台分支处理
- 屏幕仅在非 Dataverse 原生工作流中导入这些封装。Dataverse 的 File/Image 字段使用上文 File/Image Picker Ownership 一节中的
@microsoft/power-apps-native-host控件。
触觉反馈封装
对于规范化能力 haptics,按照以下契约生成 src/native/haptics.ts:
import * as Haptics from 'expo-haptics';
import { Platform } from 'react-native';
export type HapticImpactStyle = 'light' | 'medium' | 'heavy' | 'soft' | 'rigid';
export type HapticNotificationType = 'success' | 'warning' | 'error';
export type HapticResult =
| { ok: true }
| { ok: false; reason: 'unsupported' | 'error'; message: string };
const impactStyles: Record<HapticImpactStyle, Haptics.ImpactFeedbackStyle> = {
light: Haptics.ImpactFeedbackStyle.Light,
medium: Haptics.ImpactFeedbackStyle.Medium,
heavy: Haptics.ImpactFeedbackStyle.Heavy,
soft: Haptics.ImpactFeedbackStyle.Soft,
rigid: Haptics.ImpactFeedbackStyle.Rigid,
};
const notificationTypes: Record<HapticNotificationType, Haptics.NotificationFeedbackType> = {
success: Haptics.NotificationFeedbackType.Success,
warning: Haptics.NotificationFeedbackType.Warning,
error: Haptics.NotificationFeedbackType.Error,
};
async function runHaptic(effect: () => Promise<void>): Promise<HapticResult> {
if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
return {
ok: false,
reason: 'unsupported',
message: 'Haptic feedback is available only on iOS and Android.',
};
}
try {
await effect();
return { ok: true };
} catch (error) {
return {
ok: false,
reason: 'error',
message: error instanceof Error ? error.message : 'The native haptic call failed.',
};
}
}
export function triggerImpact(style: HapticImpactStyle = 'medium'): Promise<HapticResult> {
return runHaptic(() => Haptics.impactAsync(impactStyles[style]));
}
export function triggerNotification(
type: HapticNotificationType = 'success'
): Promise<HapticResult> {
return runHaptic(() => Haptics.notificationAsync(notificationTypes[type]));
}
export function triggerSelection(): Promise<HapticResult> {
return runHaptic(() => Haptics.selectionAsync());
}
只有当触觉反馈出现在已批准的 native-capabilities 和逐屏计划中时,屏幕才调用此封装。根据语义选择反馈:
triggerImpact('light' | 'medium' | 'heavy' | 'soft' | 'rigid')用于有意的按压或具有实体触感的操作。triggerSelection()用于选择器、分段控件或其他选择项发生变化时。- 在确认相应结果后,调用
triggerNotification('success' | 'warning' | 'error')。 - 视觉反馈仍应是主要信号。检查非 OK 结果并记录日志,但不要因为触觉反馈不可用,就让原本成功的业务操作失败。
操作成功后在屏幕中的用法:
setSaveState({ kind: 'success', message: 'Saved' });
const hapticResult = await triggerNotification('success');
if (!hapticResult.ok) {
console.warn('[haptics] success feedback unavailable', hapticResult);
}
expo-haptics 无需请求权限,也无需更改 app.config.js。
其他内联封装
请查阅模块发布的 API 文档(链接位于其 npm 页面),了解方法签名和权限模式。使用以下安全存储骨架作为判别联合结构的规范示例,然后将其转换为目标模块的 API。
安全存储规范骨架:
// src/native/secureStore.ts
import * as SecureStore from 'expo-secure-store';
import { Platform } from 'react-native';
export type SecureResult<T> =
| { ok: true; value: T }
| { ok: false; reason: 'unsupported' | 'not-found' | 'error'; message?: string };
export async function getSecret(key: string): Promise<SecureResult<string>> {
if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
return { ok: false, reason: 'unsupported', message: 'SecureStore is not available on this platform.' };
}
try {
const value = await SecureStore.getItemAsync(key);
if (value === null) return { ok: false, reason: 'not-found' };
return { ok: true, value };
} catch (e: any) {
return { ok: false, reason: 'error', message: e?.message };
}
}
export async function setSecret(key: string, value: string): Promise<SecureResult<true>> {
if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
return { ok: false, reason: 'unsupported', message: 'SecureStore is not available on this platform.' };
}
try {
await SecureStore.setItemAsync(key, value);
return { ok: true, value: true };
} catch (e: any) {
return { ok: false, reason: 'error', message: e?.message };
}
}
第 6 步 — 类型检查
遥测检查点:validate_native_wrapper
开始前打印:
"→ 正在运行 tsc,以验证封装可编译(约 10–20 秒)。"
cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
npx --no-install tsc --noEmit
仅修复范围内的封装侧错误。类型检查后,执行共享兼容性契约的
第 4 步:重新检查所有请求的产物、API/行为和存储义务,然后再返回成功结果。仅
类型检查成功并不足够。正常使用 /add-native 时不要运行平台专属的原生构建
命令,因为该模块已包含在当前模板二进制文件中。模板/基础版本维护者首次添加
expo-haptics 或更改其版本时,必须重新构建 Android 和 iOS 基础二进制文件,
以便 Expo 自动链接包含该模块。
第 7 步 — 总结
在总结之前返回共享兼容性结果;使用实际创建/更新/复用的路径,不要声称已完成尚未解决的产物。
✅ Native wrapper generated: <capability>
─────────────────────────────────────────────
Module (template-shipped) : <expo-module>@<version-from-package.json>
Wrapper created : src/native/<capability>.ts
package.json : unchanged ✓
app.config.js : unchanged ✓
Type-check: PASS
Sample usage:
import { takePhoto } from '../../src/native/camera';
const result = await takePhoto();
if (result.ok) {
setPhotoUri(result.uri);
} else if (result.reason === 'permission-denied') {
showToast('Camera permission required');
}
⚠️ No per-app native rebuild required when using a binary produced from the current
template. The underlying native module is already linked; wrapper edits are picked
up by Metro. Template/base dependency changes require a new native base build.
─────────────────────────────────────────────
备注
- 此技能不会修改
package.json、app.config.js、src/playerConfig.ts、src/generated/或任何屏幕文件。 - 对于支持表中未列出的功能(
expo-notifications、Bluetooth、NFC、BLE、AR——模板添加这些功能前),请告知用户模板目前尚未提供这些功能,并在上游模板仓库中提交需求。不要尝试自行安装或配置。 - 纯 JavaScript 库不在此技能的适用范围内。
/create-mobile-app或/edit-app会通过shared/references/javascript-dependency-planning.md选择并安装这些库;无需原生封装,也无需重新构建 Android/iOS。上述禁令仅适用于包含原生源码或配置的包。