上传处理
上传处理在图片保存前运行。下面把超过 1280 像素宽的静态图片缩小,后续步骤及存储接收到缩小后的内容。
最小实现
按快速开始创建项目,将 plugin.json.kind 设为 processor,保留自己的 ID 和版本。将 src/index.ts 替换为以下外层代码,再用下一段实现替换注释行:
import { Readable } from "node:stream";
import {
PluginError,
withAbortSignal,
type Definition,
} from "@picloom/plugin-sdk";
import manifest from "../plugin.json";
const { kind, apiVersion } = manifest;
if (kind !== "processor" || apiVersion !== 1)
throw new Error("Expected processor plugin API 1");
export default {
id: manifest.id,
version: manifest.version,
kind,
apiVersion,
// 将下一段实现放在这里
} satisfies Definition<"processor">;defaultLocale: "en-US",
nameKey: "package.name",
messages: {
"en-US": {
"package.name": "Example resize",
"settings.width.label": "Maximum width",
},
"zh-CN": { "package.name": "缩放示例", "settings.width.label": "最大宽度" },
},
stages: {
upload: {
configSchemaVersion: 1,
configSchema: {
type: "object",
properties: {
width: {
"x-ui": { labelKey: "settings.width.label" },
type: "integer",
minimum: 1,
default: 1280,
},
},
required: ["width"],
additionalProperties: false,
},
paramsSchema: {
type: "object",
properties: {
width: { type: "integer", minimum: 1, maximum: 8192 },
},
additionalProperties: false,
},
async createInstance({ config, services }) {
return {
async check({ file }) {
return !file.metadata.animated &&
["image/jpeg", "image/png", "image/webp"].includes(
file.metadata.mimeType ?? "",
)
? { status: "applicable" }
: {
status: "unsupported",
reason: "Only static JPEG, PNG and WebP images are supported",
};
},
async process({ file, params, limits, createOutput }, { signal }) {
const width = Number(params.width ?? config.width);
if (width > limits.maxDimension)
throw new PluginError(
"RESOURCE_LIMIT",
"Width exceeds processing limit",
);
signal.throwIfAborted();
const bytes = await new Response(
await file.openRead(),
).arrayBuffer();
const output = await createOutput();
// Sharp is supplied by the host; the package does not bundle its own copy.
const image = services.images
.sharp(bytes, { limitInputPixels: limits.maxPixels })
.rotate()
.resize({ width, withoutEnlargement: true })
.toFormat(
file.metadata.mimeType === "image/jpeg"
? "jpeg"
: file.metadata.mimeType === "image/webp"
? "webp"
: "png",
);
await withAbortSignal(image, signal, () =>
output.write(
Readable.toWeb(image) as unknown as ReadableStream<Uint8Array>,
),
);
signal.throwIfAborted();
return { status: "replace", output };
},
};
},
},
},check 判断格式,process 通过宿主 Sharp 缩放,再写入本次输出。示例将输入读入内存,不放大小图。
安装与验证
在独立项目中运行 bun run build,使用生成的 dist/<插件ID>-<版本>.zip。 后台选择示例字典中的名称(随界面语言显示),或你自己修改后的名称:
- 安装生成的 ZIP,创建上传流水线,添加 Example resize。
- 将固定宽度设为
1280,关联到一个上传配置,先不启用请求参数。 - 上传宽于 1280 像素的静态 JPEG、PNG 或 WebP,打开不带处理参数的链接,确认宽度为 1280。
本例不放大小图;测试时不要同时配置访问缩放,避免混淆保存结果与访问结果。
接口行为
check:决定是否执行
核心对当前图片调用检查;只有返回 applicable 才调用本步骤的 process。
check(input: {
params: Parameters;
file: FileInput;
readonly record: FileContext;
limits: { maxDimension: number; maxPixels: number };
}, context: UploadCheckContext): Promise<{
status: "applicable" | "noop" | "unsupported";
reason?: string;
}>;返回对象必须含 status,可选 reason: string 说明原因:
status | 核心行为 |
|---|---|
applicable | 调用本步骤的 process |
noop | 不处理,直接进入下一步 |
unsupported | 根据步骤配置跳过或中止;中止时使用 reason 说明原因 |
检查阶段不能创建输出,也不能写入 context.data。执行异常应抛错,不要用 unsupported 隐藏失败。
limits.maxDimension 是最大单边像素数,maxPixels 是最大像素总数。请求参数已经筛选并校验,不自动合并固定配置。
process:执行处理
检查通过后调用,固定配置通过实例闭包使用,本次参数来自 input.params。
process(input: {
params: Parameters;
file: FileInput;
readonly record: FileContext;
limits: { maxDimension: number; maxPixels: number };
createOutput(): Promise<Output>;
}, context: UploadProcessContext): Promise<
| { status: "pass"; organization?: OrganizationUpdate }
| { status: "replace"; output: Output; organization?: OrganizationUpdate }
| { status: "reject"; reason: string }
>;| 返回形式 | 必需字段 | 核心行为 |
|---|---|---|
| 保留 | status: "pass" | 当前图片不变,继续下一步 |
| 替换 | status: "replace"、output: Output | 校验输出并更新当前图片,继续下一步 |
| 拒绝 | status: "reject"、reason: string | 中止本次上传并报错 |
输出来自本次 createOutput(),写入完成后才能返回。输出接口见 文件与输出。
输入沿用 check 的字段,并增加 createOutput()。同一步检查与处理收到相同的 record 快照,后续步骤和路径插件读取此前成功步骤接受的归类。check 通过 context.organization 查找分类,process 还可复用或创建分类,见归类服务。
返回 organization: { tagIds, collectionIds } 设置完整目标关系;省略字段保留该项,空数组清空。每项最多 100 个 ID,仅可选择当前归属范围内的分类。核心完整校验图片输出及归类后才同时接受;无效结果不会部分更新。仅需归类时返回 { status: "pass", organization: { tagIds: [tag.id] } }。
可选能力与约束
管理员在步骤中启用 width 后,本次上传可通过 params 指定宽度;不传则使用固定配置。见 配置与请求参数示例。
输出只能写一次,必须是本次调用通过 createOutput() 创建的对象。核心检查图片和尺寸后才接受替换。check 不提供输出创建能力,它的 context.data 也只读。
示例将输入读入内存,便于理解。大文件处理应评估内存或采用流式实现,并遵守宿主传入的 limits 和取消信号。网络异常应抛错,不要返回 unsupported 隐藏失败。