关于 add-pdf-report
用于在 Power Apps 移动项目中添加或检查本地 PDF 报告封装的内部技能。它通过 `expo-print` 将应用自有 HTML 生成为 PDF;仅在已安装 `expo-sharing` 时支持分享。
主要功能
- 01生成本地 PDF 文件
- 02构建确定性的报告 HTML
- 03可选使用 expo-sharing 分享
- 04支持上传到 Dataverse File 列
- 05检查兼容性和类型错误
适用场景
- 01根据应用记录创建报告
- 02生成收据或证书
- 03预览或上传生成的 PDF
- 04在已有 expo-sharing 时添加分享
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ microsoft/ power-platform-skills --skill add-pdf-report安装后在 Claude Code 中输入/add-pdf-report确认已加载。
详细介绍
共享说明:shared-instructions.md - 请先阅读。
工作目录: 在读取任何项目内容或执行任何命令之前,先执行
native-artifact-compatibility.md Step 0。
继承 /add-native 解析出的绝对 working_dir;将每次 shell 调用和文件工具都绑定到该目录,即使此辅助程序从其他目录启动。
添加 PDF 报告
内部辅助程序。 用户应调用 /add-native pdf-report、/add-native generate-pdf 或 /add-native pdf-export;/add-native 在解析功能后会路由到此处。
为应用拥有的、由记录、证据、证书、收据或摘要生成的 PDF,生成或验证本地 PDF 报告封装。此辅助程序使用 expo-print 创建本地 PDF 文件 URI。仅当项目中已存在 expo-sharing 时才可使用它。它绝不会安装软件包,也不会直接导入原生 PDF 查看器。
功能边界
| 用户需求 | 正确路径 |
|---|---|
| 根据应用数据生成/导出/打印报告 | 此辅助程序:expo-print -> 本地 PDF URI |
| 从设备分享生成的本地 PDF | 仅当 expo-sharing 已在 package.json 中时才添加分享方法 |
| 在 Dataverse 中保留生成的 PDF | 先创建/更新父行,然后使用生成的服务上传到 Dataverse File 列 |
| 在 Power Apps 原生查看器中打开现有 HTTPS 或本地文件 PDF | /add-native pdf-viewer,且仅当项目中已存在 @microsoft/power-apps-native-pdf-viewer 0.2.9+ 时 |
| 选择/导入/上传用户选定的 PDF | /add-native document-picker 或用于 Dataverse File 列的宿主 <FilePicker> |
本地生成的 PDF 通常是 file:// URI,可通过 @microsoft/power-apps-native-pdf-viewer 0.2.9+ 传递给 openHttpsPdf(...)。
步骤
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 && test -d src || { echo "BLOCKED: working_dir is not an initialized app" >&2; exit 1; }
如果失败,请告知用户先运行 /create-mobile-app,然后停止。
2. 验证软件包是否已存在
expo-print 是必需项。除非计划明确需要分享功能,否则 expo-sharing 为可选项。
cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
node -e "const p=require('./package.json'); const deps={...p.dependencies,...p.devDependencies}; const required='expo-print'; if (!deps[required]) { console.error('MISSING: expo-print is not in package.json. The template/app must already ship it for /add-native pdf-report. This skill will not install it or edit native config. Capability not added.'); process.exit(1); } console.log('OK: expo-print package present'); console.log(deps['expo-sharing'] ? 'OK: expo-sharing package present' : 'OPTIONAL_MISSING: expo-sharing is not in package.json; generated PDFs can be created/viewed/uploaded, but sharing helpers must not be generated.');"
如果缺少 expo-print,立即停止。不要运行 npm install、npx expo install、pod install,也不要编辑 app.config.js。不要将 pdf-report 添加到此应用的计划或生成的封装中。
如果缺少 expo-sharing:
- 对于仅生成、原生查看器预览或 Dataverse 上传流程,继续执行。
- 不要导入
expo-sharing。 - 不要生成
sharePdfReport(...)。 - 如果用户的需求明确包含分享,立即停止,并说明此模板不支持分享。
2a. 协调所请求的产物
在进行任何写入或复用之前,阅读并执行 native-artifact-compatibility.md
中 PDF 报告对应行的步骤 1–3。继承当前 /add-native 的批准/模式;不要重复有效的范围限定批准。
检查 src/native/pdfReport.ts,确认包含完整的所请求 API:createPdfReport、
wrapPdfDocument、escapePdfHtml、用于保留内容的所请求 includeBase64 输出,
以及在分享获批时所需的 sharePdfReport(...)/PdfShareResult。仅生成兼容性不满足分享/上传请求。验证本地文件和生成的 File 服务负载要求;如果缺少必需存储,或范围外的产物与要求不兼容,则将 NEEDS_CONTEXT 返回给负责人,或在独立执行时询问用户。
3. 编写或验证 src/native/pdfReport.ts
应用 Step 2a 对 src/native/pdfReport.ts 的决定:按要求在文件缺失时创建;仅当兼容时原样复用;或仅进行批准范围内的更新。保留自定义 HTML 逻辑和现有导出;示例不是覆盖模板。
封装模块 MUST:
- 仅在 Step 2 确认存在
expo-print后才导入。 - 仅在 Step 2 确认存在
expo-sharing后才导入。 - 返回可区分联合类型,且绝不抛出异常。
- 在查看、分享或上传流程中,将生成的本地 PDF 视为本地文件。
- 绝不从此封装模块直接导入
@microsoft/power-apps-native-pdf-viewer。 - 保持 HTML 生成具有确定性且由应用负责;不要在封装模块内获取远程 HTML。
当请求分享且存在 expo-sharing 时使用的基础封装模块:
// src/native/pdfReport.ts
import * as Print from 'expo-print';
import * as Sharing from 'expo-sharing';
export type PdfReportResult =
| { ok: true; uri: string; numberOfPages?: number; base64?: string }
| { ok: false; reason: 'EMPTY_HTML' | 'PRINT_FAILED'; message?: string };
export type PdfShareResult =
| { ok: true }
| { ok: false; reason: 'INVALID_URI' | 'SHARING_UNAVAILABLE' | 'SHARE_FAILED'; message?: string };
export function escapePdfHtml(value: string): string {
return value
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
export function wrapPdfDocument(input: { title: string; bodyHtml: string; styles?: string }): string {
const title = escapePdfHtml(input.title.trim() || 'Report');
return `<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>${title}</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 32px; color: #111827; }
h1, h2, h3 { margin: 0 0 12px; }
table { width: 100%; border-collapse: collapse; }
th, td { border-bottom: 1px solid #e5e7eb; padding: 8px; text-align: left; }
${input.styles ?? ''}
</style>
</head>
<body>${input.bodyHtml}</body>
</html>`;
}
export async function createPdfReport(
html: string,
options?: { includeBase64?: boolean },
): Promise<PdfReportResult> {
if (!html.trim()) {
return { ok: false, reason: 'EMPTY_HTML', message: 'PDF report HTML is empty.' };
}
try {
const result = await Print.printToFileAsync({
html,
base64: options?.includeBase64 ?? false,
});
return {
ok: true,
uri: result.uri,
numberOfPages: result.numberOfPages,
base64: result.base64,
};
} catch (error: any) {
return { ok: false, reason: 'PRINT_FAILED', message: error?.message ?? String(error) };
}
}
export async function sharePdfReport(uri: string, options?: { dialogTitle?: string }): Promise<PdfShareResult> {
if (!uri || !uri.startsWith('file://')) {
return { ok: false, reason: 'INVALID_URI', message: 'Generated PDF reports must be shared from a local file URI.' };
}
try {
const available = await Sharing.isAvailableAsync();
if (!available) {
return { ok: false, reason: 'SHARING_UNAVAILABLE', message: 'Sharing is not available on this platform.' };
}
await Sharing.shareAsync(uri, {
mimeType: 'application/pdf',
UTI: 'com.adobe.pdf',
dialogTitle: options?.dialogTitle ?? 'Share PDF report',
});
return { ok: true };
} catch (error: any) {
return { ok: false, reason: 'SHARE_FAILED', message: error?.message ?? String(error) };
}
}
对于新的仅生成封装模块(或缺少 expo-sharing 时),省略 expo-sharing 导入、PdfShareResult 和 sharePdfReport(...)。保留 createPdfReport(...)、wrapPdfDocument(...) 和 escapePdfHtml(...)。不要仅因为本次未请求分享,就移除兼容的现有分享导出;任何不兼容的现有导入均须遵循 Step 2a 的范围更新门槛。
4. 使用封装模块
面向负责人的集成指南,不是在此处编辑屏幕的指示。屏幕导入封装模块,而不是直接导入 Expo 模块。仅在已批准且可用时,此示例才包含分享:
import { createPdfReport, escapePdfHtml, sharePdfReport, wrapPdfDocument } from '@/native/pdfReport';
const html = wrapPdfDocument({
title: 'Inspection report',
bodyHtml: `<h1>Inspection report</h1><p>${escapePdfHtml(summary)}</p>`,
});
const report = await createPdfReport(html);
if (!report.ok) {
showError(report.message ?? 'Report PDF was not generated.');
return;
}
const share = await sharePdfReport(report.uri, { dialogTitle: 'Share inspection report' });
if (!share.ok) {
showError(share.message ?? 'Report PDF was generated but could not be shared.');
}
如果缺少 expo-sharing,屏幕仍可调用 createPdfReport(...),通过原生 PDF viewer 0.2.9+ 预览返回的 file:// URI,或通过生成的服务将其上传到 Dataverse File 列。不得显示 Share 按钮。
5. 可选的 Dataverse 上传
可选意味着未请求;如果已批准保留,则不能跳过。Step 2a 会验证封装模块对负载的支持以及生成的签名;负责人需提供下方特定于应用的适配器/屏幕集成。若缺少由辅助程序负责的支持,或所需存储未知,则返回 NEEDS_CONTEXT,而不是仅捕获成功。
保留的 PDF 使用 Dataverse File 列。先保存或更新父行,验证 success,然后上传与生成服务的 upload(id, columnName, file, fileDisplayName?) 签名兼容的负载。绝不要将 File 列字节放入 create/update JSON。
const save = await Cr123_inspectionService.update(inspectionId, {
cr123_reportgeneratedat: new Date().toISOString(),
});
if (!save.success) {
showError(save.error?.message ?? 'Inspection was not saved.');
return;
}
const report = await createPdfReport(html, { includeBase64: true });
if (!report.ok) {
showError(report.message ?? 'Report PDF was not generated.');
return;
}
if (!report.base64) {
showError('Report PDF was generated but could not be prepared for upload.');
return;
}
// Convert report.base64 into the File/blob/picked-file shape expected by the generated service.
// Do not pass report.uri unless the generated service explicitly documents URI support.
const reportFile = createUploadFileFromBase64(report.base64, 'inspection-report.pdf', 'application/pdf');
const upload = await Cr123_inspectionService.upload(
inspectionId,
'cr123_reportfile',
reportFile,
'inspection-report.pdf',
);
if (!upload.success) {
showError(upload.error?.message ?? 'Report PDF was not uploaded.');
}
createUploadFileFromBase64(...) 有意保持为特定于应用的实现,因为生成的上传辅助程序可能需要浏览器 File、宿主 PickedFileInfo、字节或其他特定于服务的负载形状。使用为该表生成的模型/服务类型,不要编辑生成的服务。
6. 类型检查
cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
npx --no-install tsc --noEmit
仅修复范围内的辅助程序错误,然后执行共享兼容性契约的 Step 4。重新检查请求的生成、分享和保留负载,包括空 HTML、分享不可用、打印/分享失败以及缺少必需的 base64。 仅类型检查成功并不足够;不要修复屏幕/生成的文件。
7. 原生重建说明
此技能不会安装原生代码。如果刚在技能之外添加了 expo-print 或 expo-sharing,应用需要在此工作流之外进行原生重建。如果这些软件包已包含在构建中,Metro 热重载足以应用封装模块的修改。
8. 总结
在此总结之前,返回共享兼容性结果以及实际创建/更新/复用的路径。 单独报告负责人侧的上传/集成;仅在成功后更新 memory-bank;若由流程编排,则将最终更新留给负责人。
告知用户:
PDF report helper added
Required package : expo-print
Optional share : expo-sharing <present | absent>
Wrapper : src/native/pdfReport.ts
Output : local PDF file URI
Native viewer : optional; 0.2.9+ can open the generated file:// URI
Type-check : PASS
Native rebuild : not performed by this skill
在 Controls 下更新 memory-bank.md:
- PDF report helper added - expo-print local generation, expo-sharing <present|absent> (<ISO date>)