mcpField
A
add-pdf-report

add-pdf-report

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

关于 add-pdf-report

用于在 Power Apps 移动项目中添加或检查本地 PDF 报告封装的内部技能。它通过 `expo-print` 将应用自有 HTML 生成为 PDF;仅在已安装 `expo-sharing` 时支持分享。

主要功能

  1. 01生成本地 PDF 文件
  2. 02构建确定性的报告 HTML
  3. 03可选使用 expo-sharing 分享
  4. 04支持上传到 Dataverse File 列
  5. 05检查兼容性和类型错误

适用场景

  1. 01根据应用记录创建报告
  2. 02生成收据或证书
  3. 03预览或上传生成的 PDF
  4. 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, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}

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>)

评论

支持 Markdown