插件开发入门

照片流插件开发教程

从一个可运行的示例开始,为照片流添加插件页面、工具面板和设置,再通过 Host API 读取素材、运行任务和保存结果。

Host API沙箱 UIJSON Lines 服务最小权限独立发布
01

快速开始

运行你的第一个插件

照片流把可选扩展包称为“组件”,网站中沿用更熟悉的“插件”。插件不能导入照片流 React 渲染层或 Electron 主进程代码,只能通过公开桥接、组件自有 RPC 和清单授权的 Host 能力协作。

清单、页面示例和 examples/hello-component/service.cjs 均使用 sample.media-page.v1;先完整复制示例,再添加自己的 RPC。

  1. 1
    复制示例

    examples/hello-component 开始;面板、项目读写和声明式设置另有独立最小示例。

  2. 2
    选择需要的接口

    从 API 参考中选择功能,例如用 project.media.page 读取媒体列表,并查阅 SDK 中的请求参数。

  3. 3
    选择宿主入口

    有页面的插件声明工具栏或面板入口,并引用包内 component.fullPage。只提供文件预览解码器的插件可使用空的 contributions;只提供普通设置表单的插件可以省略工具入口和服务。

  4. 4
    填写权限和方法清单

    逐项列出 RPC、Host 能力、权限和事件。声明能力不会自动获得权限,每次调用都会复核。

  5. 5
    独立验证示例服务

    在照片流源码根目录运行 node scripts/mock-component-service.cjs examples/hello-component/service.cjs。该 mock 专门验证示例 RPC;自定义方法需要配套修改测试。

区分两类调用页面通过插件自有 RPC 调用服务,例如 sample.media-page.v1;服务通过 project.media.page 等 Host API 访问项目资源。
02

插件结构

准备插件文件和清单

hello-component/
  component.json
  service.cjs
  ui/index.html
  ui/icon.svg          # 可选,只允许 PNG 或被动 SVG

清单路径都是包内相对路径。路径穿越、URL、UNC、目录链接、文件符号链接、主动 SVG、缺失文件和未知字段都会失败。宿主不会导入插件 Electron/React 模块,插件也不能选择自己的 preload。

最小组件清单

{
  "apiVersion": 1,
  "id": "hello-component",
  "version": "1.0.0",
  "componentHost": {
    "contractVersion": 2,
    "contributions": [
      {
        "type": "workspace.toolbarAction",
        "id": "open",
        "label": "示例插件",
        "pageId": "main"
      },
      {
        "type": "component.fullPage",
        "id": "main",
        "title": "示例插件",
        "entry": "ui/index.html"
      }
    ],
    "service": {
      "protocolVersion": 1,
      "runtime": "node",
      "entrypoints": { "default": "service.cjs" },
      "rpcMethods": ["sample.media-page.v1"],
      "capabilities": ["project.media.page"],
      "permissions": ["project.media.read"],
      "events": []
    }
  }
}

此清单与上述示例服务配套,只请求媒体分页权限。UI 可直接使用 window.photoFlowComponent;若使用 SDK JavaScript 或 CSS,应复制或打包进组件自身目录,不可依赖包外的 ../../component-sdk 路径。

公开类型

component-sdk/index.d.ts 提供 Host API 请求、结果、错误、上下文和事件映射。

服务协议

服务使用 JSON Lines 交换请求和结果。stdout 用于协议通信,运行日志写入 stderr。

平台与架构

清单显式声明支持平台、CPU 架构、必需文件、入口和运行时;安装前统一校验完整性。

检查清单字段

宿主会拒绝未声明的方法和权限,以及无法识别的入口、字段或路径。

03

页面入口

添加工具栏、面板和右键菜单入口

workspace.toolbarAction

在项目工作区提供全页入口,并接收宿主绑定的安全项目上下文。

component.sidePanel

可作为浮动工具面板,或通过 workspace.folderPanel 加入常驻面板。不同文件页使用各自的实例。

media.contextAction

从媒体右键菜单打开,使用该文件页提供的访问范围和选择项。

project.contextAction

在文件页工具栏或右键菜单提供操作,接收当前选择的文件和文件夹。

project.importProvider

提供受控项目导入入口,输入与写入仍通过 Host 能力完成。

project.exportProvider

提供项目导出入口,不直接获得任意文件系统访问。

application.command

注册无项目应用命令;仅存在真实命令时,宿主才启用全局命令入口。

侧面板、右键动作、导入/导出和应用命令分别声明 rpcMethods,每个最多 128 个,且必须是 service.rpcMethods 中已有的方法。服务方法总数最多 128 个;设置页及表单 customPage 各最多 32 个。解码等 host-only 方法只给宿主调用,不能开放给普通页面。

灵感库文件页也会提供组件入口。根据 context.contentKind 区分 projectinspiration;灵感库没有项目版本树与进度能力,应关闭依赖这些能力的操作。

工具分组可选 workspace.videoToolsworkspace.imageToolsworkspace.officeTools,分别对应视频工具、图片工具和 Office 文档。只有 component.sidePanel 和 project.contextAction 可使用这些位置。项目和灵感库均提供对应入口,插件仍需自行检查选择是否适用。

只绘制面板正文标题、固定、关闭和宽度调整由照片流提供。浮动工具面板按正文高度显示,最高 90vh;常驻文件夹面板使用文件页分配的空间。不要在正文里重复添加窗口外框和控制按钮。
04

设置与样式

添加插件设置并统一页面样式

application.settingsForm

支持开关、选项、文本、数字和滑块(toggle/select/text/number/range),由照片流显示并保存。

customPage

需要账号授权、环境安装或诊断时,可把自定义页面放在普通设置表单旁。

application.settingsPage

没有声明式字段、且确实需要完整自定义交互时使用;设置 surface 没有项目身份。

component-sdk/ui.css

提供统一的颜色、间距、按钮、表单和对话框样式,便于插件与主程序保持一致。

不需要服务的纯设置插件

只提供普通设置时,可以让照片流直接生成页面,不必编写 HTML 或服务。清单中只放 application.settingsForm,且不要添加 customPage,例如:

{
  "apiVersion": 1,
  "id": "sample-preferences",
  "version": "1.0.0",
  "componentHost": {
    "contractVersion": 2,
    "contributions": [
      {
        "type": "application.settingsForm",
        "id": "settings",
        "label": "示例插件",
        "form": {
          "schemaVersion": 1,
          "groups": [
            {
              "id": "general",
              "title": "常用设置",
              "fields": [
                {
                  "id": "showHints",
                  "type": "toggle",
                  "label": "显示操作提示",
                  "default": true
                }
              ]
            }
          ]
        }
      }
    ]
  }
}

这个示例不需要 service、component.fullPage 或 workspace.toolbarAction。加入自定义页面或其他交互入口后,仍要按对应要求声明页面、服务和 RPC。

声明许可和播放设置

在 form.notices 中填写 title、description、license、sourceUrl、licenseUrl,可显示插件依赖的许可说明。链接只接受不带登录信息的 HTTPS 地址;只展示说明时可以留空 groups,但 notices 至少有一条。

视频播放后端可通过 preferenceScope:"videoPlayback" 绑定 hdrMode、toneMapping、targetPeakNits。照片流会检查字段、取值和后端能力,普通插件不能借此修改任意应用设置。具体限制见 设置接口说明

在页面中调用服务

ui/index.html 中放置 <pre id="result"></pre>,再通过 type="module" 脚本执行:

const host = window.photoFlowComponent;
const context = await host.getContext();
const page = await host.rpc('sample.media-page.v1', {});
document.querySelector('#result').textContent = JSON.stringify({ context, page }, null, 2);

主题可通过包内 SDK 的 mountUiTheme() 同步,页面销毁时取消订阅。通知需额外声明 notifications capability 与 permission;订阅自有事件前也必须在 service.events 列出相同事件名。

host.notify 只接受 tone/message/dedupeKey? 纯文本结构。HTML、URL、路径、命令、回调和 durationMs 都会拒绝;长任务使用 tasks,需要用户决定时使用 dialogs

05

面板与预览

添加常驻面板并跟随视频播放

常驻面板适合字幕、文件信息等需要一边浏览一边查看的内容。将下面的入口加入 componentHost.contributions,同时声明 subtitle-ui 页面和 subtitle.load.v1 服务方法:

{
  "type": "component.sidePanel",
  "id": "subtitles",
  "label": "字幕",
  "pageId": "subtitle-ui",
  "placement": "workspace.folderPanel",
  "rpcMethods": ["subtitle.load.v1"]
}

用户从“面板”菜单打开它,可以固定、拖动排序和调整宽度。切换目录或选择项后,用 onContextChange 更新内容;面板隐藏时释放订阅,重新显示时读取最新状态。

声明 component.panel 接口及同名权限后,可调用 host.setPanelInfo({title, subtitle}) 更新顶部文字。不要在正文中再添加一层标题栏。

显示播放位置,并从字幕跳转

服务清单声明 project.preview,权限声明 project.preview.read;需要跳转时加上 project.preview.control。页面通过 window.photoFlowComponent 调用:

const host = window.photoFlowComponent;
const snapshot = await host.getPreview();
if (snapshot.video?.canSeek) {
  await host.seekPreview(snapshot.video.sessionId, 0);
}

await host.onPreviewChange(callback) 持续接收状态,返回值是取消订阅函数。每次先检查 video 是否为 null;sessionId 改变时重新匹配内容。time 和 duration 以秒为单位,位置通知约每 250 毫秒一次。

只提供文件解码的插件

componentHost.service.previewDecoders 中声明支持的后缀和解码方法,可以不创建页面。宿主传入读取令牌、pageIndex 和 maxEdge;插件解析文件,将指定页转成 PNG,再通过 component.transfer 返回输出令牌。

解码器需要 project.input.tokens、component.transfer 接口及 project.input.read 权限。只返回 {inputToken, mimeType:"image/png", pageIndex, pageCount},不返回 HTML 或路径。大小、页数和权限限制见 预览接口说明

06

文件读取

读取自定义文件和传输大文件

图片编辑器、字体工具或文档插件可以用 project.files.inputToken 读取当前范围内的普通文件。清单同时声明 project.files.inputToken、project.input.tokens 两项接口,以及 project.files.read、project.input.read 两项权限。

下面的代码在服务中执行;host 来自 SDK 的 createServiceHostClient,parentId 是正在处理的 RPC 请求 ID:

const file = await host.callHost(parentId, 'project.files.inputToken', {
  relativePath: '文档/设计稿.psd'
});
const snapshot = await host.callHost(parentId, 'project.input.tokens', {
  action: 'materialize',
  token: file.input.token
});
// 服务读取 snapshot.privatePath,不把这个路径发送给页面。

需要发现文件修改或改名时,用 project.files.watch 订阅并定时 poll。每次保存返回的 cursor,收到 rescanRequired:true 后重新核对内容;监听本身不提供文件读取权限。

页面必须传送大块二进制时,用 component.transfer 分段发送,每块最多 1 MiB。每块通过独立 RPC 转发,发送成功后再继续下一块;完成后核对 SHA-256,并关闭传输会话。详见 文件资源与传输限制

07

后端服务

编写与照片流通信的插件服务

  1. ready初始化完成后输出 {"type":"ready","protocolVersion":1}
  2. request接收请求 ID、方法名、JSON 参数和当前页面信息。保留请求 ID,返回结果和调用 Host API 时会用到它。
  3. capability调用 Host API 时,用 parentId 指向当前请求,并等待 capability-response 返回结果。
  4. response返回成功或错误结果。stdout 只输出协议消息,运行日志写入 stderr。
长任务需要明确的生命周期普通请求 60 秒超时,单条消息最多 2 MiB。project.media.process 与 component.runtime.execute 可由宿主延长等待时间。长任务应显示进度,并提供取消等独立操作。

运行插件自带的程序

在服务的能力和权限中同时声明 component.runtime.execute,并通过顶层 runtimeCommandCapabilities、组件 capability 和平台入口声明自己的命令。服务用 execute 传入 runtime capability、参数及受限输入,不能从 UI 传入任意可执行文件路径。

后台执行使用 task.background:true 和稳定的 operationKeyidempotencyKey;以相同身份调用 status/cancel/pause/resume。进度事件必须预先声明;取消与暂停需要运行时支持控制参数。可参考 extensions/video-tools/service.cjs

project.media.process 提供 video.timelineFramesoffice.extractImages。视频转码、切割和裁剪由组件自己的运行时实现。

UI 运行在沙箱中;组件服务、生命周期脚本和可执行文件以当前用户的系统权限运行。Host capability/permission 是接口契约,不能把受监管服务描述为能够安全执行不受信代码的系统沙箱。

08

媒体处理流程

读取素材、保存结果并创建版本

  1. 1
    分页读取

    先用 project.media.page 获取媒体引用,再调用 project.media.variants 并传入 variants:[] 获取稳定元数据。

  2. 2
    按需授权像素

    需要时再请求 thumbnail、preview 或 original;原图附带短时一次性输入令牌。

  3. 3
    取得文件副本

    通过 project.input.tokens 把受限输入复制进插件私有存储。

  4. 4
    暂存与校验

    调用 project.output 的 stage、write 和 validate,目标始终是项目相对路径。

  5. 5
    保存处理结果

    使用固定的 idempotencyKey 提交,同一次操作重试时不换键。新文件重名时可使用 onConflict:"rename",并读取返回的实际保存路径。

  6. 6
    创建版本

    可把 commit/artifact ID 交给 version.create,并使用另一个稳定幂等键。

  7. 7
    恢复或清理

    stage 保留 24 小时;重启后继续校验/提交,放弃时显式 rollback。

token 是临时读取凭证,cursor 用于继续翻页,stage 是暂存区,commit/artifact 表示已保存的结果。使用宿主返回的值,不要自行构造或跨项目复用。幂等键用于识别同一次保存操作,重试时保持不变。

09

本地开发

在本地加载和调试插件

未打包开发环境会从项目 extensionsPHOTOFLOW_COMPONENT_DEV_ROOTS 发现组件。每个包通过自己的 package.json 选择清单、准备命令、开发运行时和文件映射;同 ID 已安装包存在时,开发构建使用当前源码。

npm run prepare:components:dev
npm run electron:dev
  • 开发映射只能指向清单已经声明的包路径,未知字段和未声明路径会拒绝。
  • Python/Node 脚本运行时可以声明命令、入口和参数前缀;原生可执行文件省略脚本入口。
  • 有效源码注册会标记为“开发组件”,不会伪装成通过完整性验证的正式安装。
  • 开发与安装组件都可以停用;停用会关闭 surface、服务、工作进程与未完成网络活动。
10

测试与打包

测试插件并打包安装

npm run test:component-host-api
npm run test:component-host
npm run test:component-service
npm run test:electron-security
npm run test:architecture
  • 用当前 component-manifest schema 校验清单,并确认所有 Host 能力名称与 SDK 一致。
  • 安装包只包含构建后的 UI、服务与运行资源;生命周期动作必须填写匹配 SHA-256。
  • 在干净配置中验证安装、停用、启用、取消、重启、任务恢复和卸载。
  • 核对包内清单、组件版本和实际文件,确保用户能按安装说明完成安装。
查阅 Host API

选择需要的接口,了解请求参数、权限和错误处理方式。

打开 API 参考