mirror of
https://github.com/immich-app/immich.git
synced 2026-06-24 15:38:11 -07:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9751530af8 | |||
| 0931a19c5c | |||
| 08b2e2c0b5 | |||
| e5b50a55a4 |
@@ -45,6 +45,8 @@ jobs:
|
||||
- 'server/**'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'mise.toml'
|
||||
- 'packages/plugin-core/**'
|
||||
- 'packages/plugin-sdk/**'
|
||||
cli:
|
||||
- 'packages/cli/**'
|
||||
- 'packages/sdk/**'
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ DB_DATA_LOCATION=./postgres
|
||||
# TZ=Etc/UTC
|
||||
|
||||
# The Immich version to use. You can pin this to a specific version like "v2.1.0"
|
||||
IMMICH_VERSION=v3
|
||||
IMMICH_VERSION=v2
|
||||
|
||||
# Connection secret for postgres. You should change it to a random password
|
||||
# Please use only the characters `A-Za-z0-9`, without special characters or spaces
|
||||
|
||||
@@ -19,7 +19,7 @@ If this does not work, try running `docker compose up -d --force-recreate`.
|
||||
|
||||
| Variable | Description | Default | Containers |
|
||||
| :----------------- | :------------------------------ | :-----: | :----------------------- |
|
||||
| `IMMICH_VERSION` | Image tags | `v3` | server, machine learning |
|
||||
| `IMMICH_VERSION` | Image tags | `v2` | server, machine learning |
|
||||
| `UPLOAD_LOCATION` | Host path for uploads | | server |
|
||||
| `DB_DATA_LOCATION` | Host path for Postgres database | | database |
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ docker image prune
|
||||
## Versioning Policy
|
||||
|
||||
Immich follows [semantic versioning][semver], which tags releases in the format `<major>.<minor>.<patch>`. We intend for breaking changes to be limited to major version releases.
|
||||
You can configure your Docker image to point to the current major version by using a metatag, such as `:v3`.
|
||||
You can configure your Docker image to point to the current major version by using a metatag, such as `:v2`.
|
||||
|
||||
Currently, we have no plans to backport patches to earlier versions. We encourage all users to run the most recent release of Immich.
|
||||
Switching back to an earlier version, even within the same minor release tag, is not supported.
|
||||
|
||||
@@ -1507,6 +1507,9 @@
|
||||
"notes": "Notes",
|
||||
"nothing_here_yet": "Nothing here yet",
|
||||
"notification_backup_reliability": "Enable notifications to improve background backup reliability",
|
||||
"notification_enabled_list_tile_content": "Immich uses notifications for background backup. Manage them in your device settings.",
|
||||
"notification_enabled_list_tile_open_button": "Open settings",
|
||||
"notification_enabled_list_tile_title": "Notifications enabled",
|
||||
"notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.",
|
||||
"notification_permission_list_tile_content": "Grant permission to enable notifications.",
|
||||
"notification_permission_list_tile_enable_button": "Enable Notifications",
|
||||
|
||||
@@ -446,7 +446,6 @@ class SyncStreamService {
|
||||
|
||||
await _syncStreamRepository.updateAssetsV1([asset], debugLabel: 'websocket-edit');
|
||||
await _syncStreamRepository.replaceAssetEditsV1(asset.id, assetEdits, debugLabel: 'websocket-edit');
|
||||
await _refreshAssetOcrAndFaces(asset.id);
|
||||
|
||||
_logger.info(
|
||||
'Successfully processed AssetEditReadyV1 event for asset ${asset.id} with ${assetEdits.length} edits',
|
||||
@@ -485,7 +484,6 @@ class SyncStreamService {
|
||||
|
||||
await _syncStreamRepository.updateAssetsV2([asset], debugLabel: 'websocket-edit');
|
||||
await _syncStreamRepository.replaceAssetEditsV1(asset.id, assetEdits, debugLabel: 'websocket-edit');
|
||||
await _refreshAssetOcrAndFaces(asset.id);
|
||||
|
||||
_logger.info(
|
||||
'Successfully processed AssetEditReadyV2 event for asset ${asset.id} with ${assetEdits.length} edits',
|
||||
@@ -495,22 +493,6 @@ class SyncStreamService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _refreshAssetOcrAndFaces(String assetId) async {
|
||||
try {
|
||||
final ocr = await _api.assetsApi.getAssetOcr(assetId);
|
||||
await _syncStreamRepository.replaceAssetOcr(assetId, ocr ?? const []);
|
||||
} catch (error, stackTrace) {
|
||||
_logger.severe("Error refreshing OCR for asset $assetId", error, stackTrace);
|
||||
}
|
||||
|
||||
try {
|
||||
final faces = await _api.facesApi.getFaces(assetId);
|
||||
await _syncStreamRepository.replaceAssetFaces(assetId, faces ?? const []);
|
||||
} catch (error, stackTrace) {
|
||||
_logger.severe("Error refreshing faces for asset $assetId", error, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleRemoteDeleted(Iterable<String> remoteIds) async {
|
||||
if (remoteIds.isEmpty) {
|
||||
return Future.value();
|
||||
|
||||
@@ -896,71 +896,6 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||
}
|
||||
}
|
||||
|
||||
/// Replaces all OCR rows for [assetId] with [data] (e.g. after an asset edit re-runs OCR).
|
||||
Future<void> replaceAssetOcr(String assetId, Iterable<AssetOcrResponseDto> data) async {
|
||||
try {
|
||||
await _db.batch((batch) {
|
||||
batch.deleteWhere(_db.assetOcrEntity, (row) => row.assetId.equals(assetId));
|
||||
|
||||
for (final ocr in data) {
|
||||
batch.insert(
|
||||
_db.assetOcrEntity,
|
||||
AssetOcrEntityCompanion(
|
||||
id: Value(ocr.id),
|
||||
assetId: Value(ocr.assetId),
|
||||
recognizedText: Value(ocr.text),
|
||||
x1: Value(ocr.x1),
|
||||
y1: Value(ocr.y1),
|
||||
x2: Value(ocr.x2),
|
||||
y2: Value(ocr.y2),
|
||||
x3: Value(ocr.x3),
|
||||
y3: Value(ocr.y3),
|
||||
x4: Value(ocr.x4),
|
||||
y4: Value(ocr.y4),
|
||||
boxScore: Value(ocr.boxScore),
|
||||
textScore: Value(ocr.textScore),
|
||||
isVisible: const Value(true),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Error: replaceAssetOcr', error, stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> replaceAssetFaces(String assetId, Iterable<AssetFaceResponseDto> data) async {
|
||||
try {
|
||||
await _db.batch((batch) {
|
||||
batch.deleteWhere(_db.assetFaceEntity, (row) => row.assetId.equals(assetId));
|
||||
|
||||
for (final face in data) {
|
||||
batch.insert(
|
||||
_db.assetFaceEntity,
|
||||
AssetFaceEntityCompanion(
|
||||
id: Value(face.id),
|
||||
assetId: Value(assetId),
|
||||
personId: Value(face.person?.id),
|
||||
imageWidth: Value(face.imageWidth),
|
||||
imageHeight: Value(face.imageHeight),
|
||||
boundingBoxX1: Value(face.boundingBoxX1),
|
||||
boundingBoxY1: Value(face.boundingBoxY1),
|
||||
boundingBoxX2: Value(face.boundingBoxX2),
|
||||
boundingBoxY2: Value(face.boundingBoxY2),
|
||||
sourceType: Value(face.sourceType.orElse(null)?.value ?? SourceType.machineLearning.value),
|
||||
isVisible: const Value(true),
|
||||
deletedAt: const Value(null),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Error: replaceAssetFaces', error, stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> pruneAssets() async {
|
||||
try {
|
||||
await _db.transaction(() async {
|
||||
|
||||
@@ -7,7 +7,6 @@ import 'package:immich_mobile/infrastructure/repositories/network.repository.dar
|
||||
import 'package:immich_mobile/models/server_info/server_version.model.dart';
|
||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/ocr.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/utils/debounce.dart';
|
||||
@@ -182,34 +181,11 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||
}
|
||||
|
||||
void _handleSyncAssetEditReadyV1(dynamic data) {
|
||||
final assetId = _assetIdFromEditReady(data);
|
||||
unawaited(
|
||||
_ref.read(backgroundSyncProvider).syncWebsocketEditV1(data).whenComplete(() => _onAssetEditApplied(assetId)),
|
||||
);
|
||||
unawaited(_ref.read(backgroundSyncProvider).syncWebsocketEditV1(data));
|
||||
}
|
||||
|
||||
void _handleSyncAssetEditReadyV2(dynamic data) {
|
||||
final assetId = _assetIdFromEditReady(data);
|
||||
unawaited(
|
||||
_ref.read(backgroundSyncProvider).syncWebsocketEditV2(data).whenComplete(() => _onAssetEditApplied(assetId)),
|
||||
);
|
||||
}
|
||||
|
||||
String? _assetIdFromEditReady(dynamic data) {
|
||||
if (data is Map && data['asset'] is Map) {
|
||||
final id = (data['asset'] as Map)['id'];
|
||||
return id is String ? id : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// The edit handler refreshes OCR/faces in the drift DB from a background isolate,
|
||||
/// so the main-isolate UI providers must be invalidated here to re-read the new data.
|
||||
void _onAssetEditApplied(String? assetId) {
|
||||
if (assetId == null) {
|
||||
return;
|
||||
}
|
||||
_ref.invalidate(ocrAssetProvider(assetId));
|
||||
unawaited(_ref.read(backgroundSyncProvider).syncWebsocketEditV2(data));
|
||||
}
|
||||
|
||||
void _processBatchedAssetUploadReadyV1() {
|
||||
|
||||
@@ -36,7 +36,6 @@ class ApiService {
|
||||
late MemoriesApi memoriesApi;
|
||||
late SessionsApi sessionsApi;
|
||||
late TagsApi tagsApi;
|
||||
late FacesApi facesApi;
|
||||
|
||||
ApiService() {
|
||||
// The below line ensures that the api clients are initialized when the service is instantiated
|
||||
@@ -78,7 +77,6 @@ class ApiService {
|
||||
memoriesApi = MemoriesApi(_apiClient);
|
||||
sessionsApi = SessionsApi(_apiClient);
|
||||
tagsApi = TagsApi(_apiClient);
|
||||
facesApi = FacesApi(_apiClient);
|
||||
}
|
||||
|
||||
Future<String> resolveAndSetEndpoint(String serverUrl) async {
|
||||
|
||||
@@ -48,6 +48,14 @@ class NotificationSetting extends HookConsumerWidget {
|
||||
showPermissionsDialog();
|
||||
}
|
||||
}),
|
||||
)
|
||||
else
|
||||
SettingsButtonListTile(
|
||||
icon: Icons.notifications_active_outlined,
|
||||
title: 'notification_enabled_list_tile_title'.tr(),
|
||||
subtileText: 'notification_enabled_list_tile_content'.tr(),
|
||||
buttonText: 'notification_enabled_list_tile_open_button'.tr(),
|
||||
onButtonTap: () => openAppSettings(),
|
||||
),
|
||||
];
|
||||
|
||||
|
||||
@@ -222,7 +222,16 @@
|
||||
"name": "assetLock",
|
||||
"title": "Move to locked folder",
|
||||
"description": "Change visibility to locked",
|
||||
"types": ["AssetV1"]
|
||||
"types": ["AssetV1"],
|
||||
"schema": {
|
||||
"properties": {
|
||||
"inverse": {
|
||||
"title": "Inverse",
|
||||
"description": "When true will unarchive any archived assets",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "assetTimeline",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "pnpm build:tsc && pnpm build:wasm",
|
||||
"build:tsc": "tsc --noEmit && node esbuild.js",
|
||||
"build:tsc": "mkdir -p dist && echo \"type Manifest = $(cat manifest.json); \nexport default Manifest;\" > dist/manifest.d.ts && tsc --noEmit && node esbuild.js",
|
||||
"build:wasm": "extism-js dist/index.js -i src/index.d.ts -o dist/plugin.wasm"
|
||||
},
|
||||
"keywords": [],
|
||||
|
||||
Vendored
+1
-1
@@ -22,6 +22,6 @@ declare module 'main' {
|
||||
export function assetArchive(): I32;
|
||||
export function assetLock(): I32;
|
||||
export function assetTimeline(): I32;
|
||||
export function assetTrash(): I32;
|
||||
// export function assetTrash(): I32;
|
||||
export function assetAddToAlbums(): I32;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { wrapper } from '@immich/plugin-sdk';
|
||||
import { AssetTypeEnum, AssetVisibility, WorkflowType } from '@immich/sdk';
|
||||
import { getWrapper } from '@immich/plugin-sdk';
|
||||
import { AssetVisibility } from '@immich/sdk';
|
||||
import type manifestType from '../dist/manifest';
|
||||
|
||||
const wrapper = getWrapper<manifestType>();
|
||||
|
||||
type AssetFileFilterConfig = {
|
||||
pattern: string;
|
||||
matchType?: 'contains' | 'exact' | 'regex' | 'startsWith';
|
||||
caseSensitive?: boolean;
|
||||
};
|
||||
export const assetFileFilter = () => {
|
||||
return wrapper<WorkflowType.AssetV1, AssetFileFilterConfig>(({ data, config }) => {
|
||||
return wrapper<'assetFileFilter'>(({ data, config }) => {
|
||||
const { pattern, matchType = 'contains', caseSensitive = false } = config;
|
||||
|
||||
const { asset } = data;
|
||||
@@ -43,7 +41,7 @@ export const assetFileFilter = () => {
|
||||
};
|
||||
|
||||
export const assetMissingTimeZoneFilter = () => {
|
||||
return wrapper<WorkflowType.AssetV1, { inverse?: boolean }>(({ config, data }) => {
|
||||
return wrapper<'assetMissingTimeZoneFilter'>(({ config, data }) => {
|
||||
const hasTimeZone = !!data.asset?.exifInfo?.timeZone;
|
||||
const needsTimeZone = config.inverse ? true : false;
|
||||
return { workflow: { continue: hasTimeZone === needsTimeZone } };
|
||||
@@ -51,13 +49,7 @@ export const assetMissingTimeZoneFilter = () => {
|
||||
};
|
||||
|
||||
export const assetLocationFilter = () => {
|
||||
return wrapper<
|
||||
WorkflowType.AssetV1,
|
||||
{
|
||||
region?: { country?: string; state?: string; city?: string };
|
||||
coordinate?: { latitude?: string; longitude?: string; radius?: number };
|
||||
}
|
||||
>(({ config, data }) => {
|
||||
return wrapper<'assetLocationFilter'>(({ config, data }) => {
|
||||
if (
|
||||
(config.region?.country && config.region.country !== data.asset.exifInfo?.country) ||
|
||||
(config.region?.state && config.region.state !== data.asset.exifInfo?.state) ||
|
||||
@@ -96,13 +88,13 @@ export const assetLocationFilter = () => {
|
||||
};
|
||||
|
||||
export const assetTypeFilter = () => {
|
||||
return wrapper<WorkflowType.AssetV1, { allowedTypes: AssetTypeEnum[] }>(({ config, data }) => {
|
||||
return wrapper<'assetTypeFilter'>(({ config, data }) => {
|
||||
return { workflow: { continue: config.allowedTypes.includes(data.asset.type) } };
|
||||
});
|
||||
};
|
||||
|
||||
export const assetFavorite = () => {
|
||||
return wrapper<WorkflowType.AssetV1, { inverse?: boolean }>(({ config, data }) => {
|
||||
return wrapper<'assetFavorite'>(({ config, data }) => {
|
||||
const target = config.inverse ? false : true;
|
||||
if (target !== data.asset.isFavorite) {
|
||||
return {
|
||||
@@ -115,13 +107,13 @@ export const assetFavorite = () => {
|
||||
};
|
||||
|
||||
export const assetVisibility = () => {
|
||||
return wrapper<WorkflowType.AssetV1, { visibility: AssetVisibility }>(({ config }) => ({
|
||||
changes: { asset: { visibility: config.visibility } },
|
||||
return wrapper<'assetVisibility'>(({ config }) => ({
|
||||
changes: { asset: { visibility: config.visibility as AssetVisibility } },
|
||||
}));
|
||||
};
|
||||
|
||||
export const assetArchive = () => {
|
||||
return wrapper<WorkflowType.AssetV1, { inverse?: boolean }>(({ config, data }) => {
|
||||
return wrapper<'assetArchive'>(({ config, data }) => {
|
||||
if (!config.inverse && data.asset.visibility !== AssetVisibility.Archive) {
|
||||
return { changes: { asset: { visibility: AssetVisibility.Archive } } };
|
||||
}
|
||||
@@ -135,7 +127,7 @@ export const assetArchive = () => {
|
||||
};
|
||||
|
||||
export const assetLock = () => {
|
||||
return wrapper<WorkflowType.AssetV1, { inverse?: boolean }>(({ config, data }) => {
|
||||
return wrapper<'assetLock'>(({ config, data }) => {
|
||||
if (!config.inverse && data.asset.visibility !== AssetVisibility.Locked) {
|
||||
return { changes: { asset: { visibility: AssetVisibility.Locked } } };
|
||||
}
|
||||
@@ -148,13 +140,13 @@ export const assetLock = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const assetTrash = () => {
|
||||
// TODO use trash/untrash host functions
|
||||
return wrapper<WorkflowType.AssetV1, { inverse?: boolean }>(() => ({}));
|
||||
};
|
||||
// export const assetTrash = () => {
|
||||
// // TODO use trash/untrash host functions
|
||||
// return wrapper<WorkflowType.AssetV1, { inverse?: boolean }>(() => ({}));
|
||||
// };
|
||||
|
||||
export const assetAddToAlbums = () => {
|
||||
return wrapper<WorkflowType.AssetV1, { albumIds: string[]; albumName?: string }>(({ config, data, functions }) => {
|
||||
return wrapper<'assetAddToAlbums'>(({ config, data, functions }) => {
|
||||
const assetId = data.asset.id;
|
||||
|
||||
if (config.albumIds.length === 0) {
|
||||
|
||||
@@ -1,53 +1,104 @@
|
||||
import type { WorkflowType } from '@immich/sdk';
|
||||
import { hostFunctions } from 'src/host-functions.js';
|
||||
import type {
|
||||
ConfigValue,
|
||||
WorkflowEventPayload,
|
||||
WorkflowResponse,
|
||||
WorkflowStepConfig,
|
||||
} from 'src/types.js';
|
||||
|
||||
export const wrapper = <
|
||||
T extends WorkflowType,
|
||||
TConfig extends ConfigValue = ConfigValue,
|
||||
>(
|
||||
fn: (
|
||||
payload: WorkflowEventPayload<T, TConfig> & {
|
||||
functions: ReturnType<typeof hostFunctions>;
|
||||
},
|
||||
) => WorkflowResponse<T> | undefined,
|
||||
) => {
|
||||
const input = Host.inputString();
|
||||
|
||||
try {
|
||||
const payload = JSON.parse(input) as WorkflowEventPayload<T, TConfig>;
|
||||
const event = {
|
||||
...payload,
|
||||
functions: hostFunctions(payload.workflow.authToken),
|
||||
};
|
||||
|
||||
const eventConfigBefore = JSON.stringify(event.config);
|
||||
|
||||
console.debug(
|
||||
`Inputs: trigger=${event.trigger}, event=${event.type}, config=${eventConfigBefore}`,
|
||||
);
|
||||
|
||||
const response = fn(event) ?? {};
|
||||
|
||||
// if config changed, notify host
|
||||
const eventConfigAfter = JSON.stringify(event.config);
|
||||
if (!response.config && eventConfigBefore !== eventConfigAfter) {
|
||||
response.config = event.config as WorkflowStepConfig;
|
||||
}
|
||||
|
||||
console.debug(
|
||||
`Outputs: workflow=${JSON.stringify(response.workflow)}, changes=${JSON.stringify(response.changes)}, data=${JSON.stringify(response.data)}, config=${JSON.stringify(response.config)}`,
|
||||
);
|
||||
|
||||
const output = JSON.stringify(response);
|
||||
Host.outputString(output);
|
||||
} catch (error: Error | any) {
|
||||
console.error(`Unhandled plugin exception: ${error.message || error}`);
|
||||
throw error;
|
||||
}
|
||||
type Property = {
|
||||
type: 'string' | 'boolean' | 'number';
|
||||
array?: boolean;
|
||||
enum?: string[];
|
||||
} & {
|
||||
type: 'object';
|
||||
properties: { [K: string]: Property };
|
||||
required?: string[];
|
||||
};
|
||||
|
||||
type RequiredProperties<
|
||||
Properties extends { [K: string]: unknown },
|
||||
Required extends string[] | undefined,
|
||||
RequiredKeys extends string = Required extends undefined
|
||||
? never
|
||||
: NonNullable<Required>[number],
|
||||
> = {
|
||||
properties: Pick<Properties, RequiredKeys> &
|
||||
Partial<Omit<Properties, RequiredKeys>>;
|
||||
};
|
||||
|
||||
type GetConfigType<T extends Property> = 'enum' extends keyof T
|
||||
? NonNullable<T['enum']>[number]
|
||||
: T['type'] extends 'boolean'
|
||||
? boolean
|
||||
: T['type'] extends 'number'
|
||||
? number
|
||||
: T['type'] extends 'string'
|
||||
? string
|
||||
: T['type'] extends 'object'
|
||||
? ConfigValue<T>
|
||||
: never;
|
||||
|
||||
type ConfigValue<
|
||||
T extends { properties: { [K: string]: Property }; required?: string[] },
|
||||
Properties extends { [K: string]: Property } = T['properties'],
|
||||
> = T extends never
|
||||
? never
|
||||
: RequiredProperties<
|
||||
{
|
||||
[K in keyof Properties]: Properties[K]['array'] extends true
|
||||
? Array<GetConfigType<Properties[K]>>
|
||||
: GetConfigType<Properties[K]>;
|
||||
},
|
||||
'required' extends keyof T ? T['required'] : undefined
|
||||
>['properties'];
|
||||
|
||||
export const getWrapper =
|
||||
<T extends Record<string, any>>() =>
|
||||
<
|
||||
K extends T['methods'][number]['name'],
|
||||
L extends WorkflowType = (T['methods'][number] & {
|
||||
name: K;
|
||||
})['types'][number],
|
||||
TConfig = ConfigValue<(T['methods'][number] & { name: K })['schema']>,
|
||||
>(
|
||||
fn: (
|
||||
payload: WorkflowEventPayload<L, TConfig> & {
|
||||
functions: ReturnType<typeof hostFunctions>;
|
||||
},
|
||||
) => WorkflowResponse<L> | undefined,
|
||||
) => {
|
||||
const input = Host.inputString();
|
||||
|
||||
try {
|
||||
const payload = JSON.parse(input) as WorkflowEventPayload<K, TConfig>;
|
||||
const event = {
|
||||
...payload,
|
||||
functions: hostFunctions(payload.workflow.authToken),
|
||||
};
|
||||
|
||||
const eventConfigBefore = JSON.stringify(event.config);
|
||||
|
||||
console.debug(
|
||||
`Inputs: trigger=${event.trigger}, event=${event.type}, config=${eventConfigBefore}`,
|
||||
);
|
||||
|
||||
const response = fn(event) ?? {};
|
||||
|
||||
// if config changed, notify host
|
||||
const eventConfigAfter = JSON.stringify(event.config);
|
||||
if (!response.config && eventConfigBefore !== eventConfigAfter) {
|
||||
response.config = event.config as WorkflowStepConfig;
|
||||
}
|
||||
|
||||
console.debug(
|
||||
`Outputs: workflow=${JSON.stringify(response.workflow)}, changes=${JSON.stringify(response.changes)}, data=${JSON.stringify(response.data)}, config=${JSON.stringify(response.config)}`,
|
||||
);
|
||||
|
||||
const output = JSON.stringify(response);
|
||||
Host.outputString(output);
|
||||
} catch (error: Error | any) {
|
||||
console.error(`Unhandled plugin exception: ${error.message || error}`);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -224,6 +224,7 @@ export class PluginRepository {
|
||||
error: (message) => logger.error(message),
|
||||
} as Console,
|
||||
logLevel: asExtismLogLevel(logger.getLogLevel()),
|
||||
enableWasiOutput: true,
|
||||
},
|
||||
),
|
||||
destroy: (plugin) => plugin.close(),
|
||||
|
||||
Reference in New Issue
Block a user