feat: workflows & plugins (#26727)

feat: plugins

chore: better types

feat: plugins
This commit is contained in:
Jason Rasmussen
2026-05-18 11:09:33 -04:00
committed by GitHub
parent 7384799f19
commit 3d075f2bf8
144 changed files with 6099 additions and 7419 deletions
+33
View File
@@ -0,0 +1,33 @@
export enum WorkflowTrigger {
AssetCreate = 'AssetCreate',
PersonRecognized = 'PersonRecognized',
}
export enum WorkflowType {
AssetV1 = 'AssetV1',
AssetPersonV1 = 'AssetPersonV1',
}
export enum AssetType {
Image = 'IMAGE',
Video = 'VIDEO',
Audio = 'AUDIO',
Other = 'OTHER',
}
export enum AssetStatus {
Active = 'active',
Trashed = 'trashed',
Deleted = 'deleted',
}
export enum AssetVisibility {
Archive = 'archive',
Timeline = 'timeline',
/**
* Video part of the LivePhotos and MotionPhotos
*/
Hidden = 'hidden',
Locked = 'locked',
}
+51
View File
@@ -0,0 +1,51 @@
declare module 'extism:host' {
interface user {
albumAddAssets(ptr: PTR): I64;
addAssetsToAlbums(ptr: PTR): I64;
}
}
const host = Host.getFunctions();
type HostFunctionName = keyof typeof host;
type HostFunctionSuccessResult<T> = { success: true; response: T };
type HostFunctionErrorResult = {
success: false;
status: number;
message: string;
};
type HostFunctionResult<T> =
| HostFunctionSuccessResult<T>
| HostFunctionErrorResult;
const call = <T, R>(name: HostFunctionName, authToken: string, args: T) => {
const pointer1 = Memory.fromString(JSON.stringify({ authToken, args }));
const fn = host[name];
const handler = Memory.find(fn(pointer1.offset));
try {
const result = JSON.parse(handler.readString()) as HostFunctionResult<R>;
if (result.success) {
return result.response;
}
throw new Error(
`Failed to call host function "${String(name)}", received ${result.status} - ${JSON.stringify(result.message)}`,
);
} finally {
handler.free();
pointer1.free();
}
};
type AlbumsToAssets = {
assetIds: string[];
albumIds: string[];
};
export const hostFunctions = (authToken: string) => ({
albumAddAssets: (albumId: string, assetIds: string[]) =>
call('albumAddAssets', authToken, [albumId, { ids: assetIds }]),
addAssetsToAlbums: ({ assetIds, albumIds }: AlbumsToAssets) =>
call('addAssetsToAlbums', authToken, [{ albumIds, assetIds }]),
});
+4
View File
@@ -0,0 +1,4 @@
export * from 'src/enum.js';
export * from 'src/host-functions.js';
export * from 'src/sdk.js';
export * from 'src/types.js';
+43
View File
@@ -0,0 +1,43 @@
import type { WorkflowType } from 'src/enum.js';
import { hostFunctions } from 'src/host-functions.js';
import type {
ConfigValue,
WorkflowEventPayload,
WorkflowResponse,
} from 'src/types.js';
export const wrapper = <
T extends WorkflowType = WorkflowType,
TConfig extends ConfigValue = ConfigValue,
>(
fn: (
payload: WorkflowEventPayload<T, TConfig> & {
functions: ReturnType<typeof hostFunctions>;
},
) => WorkflowResponse<T> | undefined,
) => {
const input = Host.inputString();
try {
const event = JSON.parse(input) as WorkflowEventPayload<T, TConfig>;
// const debug = event.workflow.debug ?? false;
console.debug(
`Inputs: trigger=${event.trigger}, event=${event.type}, config=${JSON.stringify(event.config)}`,
);
const response =
fn({ ...event, functions: hostFunctions(event.workflow.authToken) }) ??
{};
console.debug(
`Outputs: workflow=${JSON.stringify(response.workflow)}, changes=${JSON.stringify(response.changes)}, data=${JSON.stringify(response.data)}`,
);
const output = JSON.stringify(response);
Host.outputString(output);
} catch (error: Error | any) {
console.error(`Unhandled plugin exception: ${error.message || error}`);
throw error;
}
};
+129
View File
@@ -0,0 +1,129 @@
import type {
AssetStatus,
AssetType,
AssetVisibility,
WorkflowTrigger,
WorkflowType,
} from 'src/enum.js';
type DeepPartial<T> = T extends Date
? T
: T extends Record<string, unknown>
? { [K in keyof T]?: DeepPartial<T[K]> }
: T extends Array<infer R>
? DeepPartial<R>[]
: T;
export type WorkflowEventMap = {
[WorkflowType.AssetV1]: AssetV1;
[WorkflowType.AssetPersonV1]: AssetPersonV1;
};
export type WorkflowEventData<T extends WorkflowType> = WorkflowEventMap[T];
export type WorkflowEventPayload<
T extends WorkflowType = WorkflowType,
TConfig = WorkflowStepConfig,
> = {
trigger: WorkflowTrigger;
type: T;
data: WorkflowEventData<T>;
config: TConfig;
workflow: {
id: string;
authToken: string;
stepId: string;
debug?: boolean;
};
};
export type WorkflowChanges<T extends WorkflowType = WorkflowType> =
DeepPartial<WorkflowEventData<T>>;
export type WorkflowResponse<T extends WorkflowType = WorkflowType> = {
workflow?: {
/** stop the workflow */
continue?: boolean;
};
changes?: WorkflowChanges<T>;
/** data to be passed to the next workflow step */
data?: Record<string, unknown>;
};
export type WorkflowStepConfig = {
[key: string]: ConfigValue;
};
export type ConfigValue =
| string
| number
| boolean
| null
| ConfigValue[]
| { [key: string]: ConfigValue };
export type AssetV1 = {
asset: {
id: string;
ownerId: string;
type: AssetType;
originalPath: string;
fileCreatedAt: Date;
fileModifiedAt: Date;
isFavorite: boolean;
checksum: Buffer; // sha1 checksum
livePhotoVideoId: string | null;
updatedAt: Date;
createdAt: Date;
originalFileName: string;
isOffline: boolean;
libraryId: string | null;
isExternal: boolean;
deletedAt: Date | null;
localDateTime: Date;
stackId: string | null;
duplicateId: string | null;
status: AssetStatus;
visibility: AssetVisibility;
isEdited: boolean;
exifInfo: {
make: string | null;
model: string | null;
exifImageWidth: number | null;
exifImageHeight: number | null;
fileSizeInByte: number | null;
orientation: string | null;
dateTimeOriginal: Date | null;
modifyDate: Date | null;
lensModel: string | null;
fNumber: number | null;
focalLength: number | null;
iso: number | null;
latitude: number | null;
longitude: number | null;
city: string | null;
state: string | null;
country: string | null;
description: string | null;
fps: number | null;
exposureTime: string | null;
livePhotoCID: string | null;
timeZone: string | null;
projectionType: string | null;
profileDescription: string | null;
colorspace: string | null;
bitsPerSample: number | null;
autoStackId: string | null;
rating: number | null;
tags: string[] | null;
updatedAt: Date | null;
} | null;
};
};
export type AssetPersonV1 = AssetV1 & {
person: {
id: string;
name: string;
};
};