feat: expose scheduled notifications to plugins

This commit is contained in:
2026-07-14 02:37:28 +08:00
parent 67753927cb
commit 96dd499e0b
5 changed files with 174 additions and 0 deletions
@@ -220,6 +220,24 @@ export function createPluginAPI(pluginId) {
}
},
notifications: {
replace: function(items) {
assertActive('notifications.replace');
if (!Array.isArray(items)) {
throw new Error('notifications.replace requires an array');
}
return callBackendErrorString(pluginId, 'notifications.replace', function() {
return App.ReplacePluginNotifications(pluginId, items);
});
},
clear: function() {
assertActive('notifications.clear');
return callBackendErrorString(pluginId, 'notifications.clear', function() {
return App.ClearPluginNotifications(pluginId);
});
}
},
settings: {
read: async function(key) {
assertActive('settings.read');
@@ -3,6 +3,7 @@ import path from 'node:path';
import { pathToFileURL } from 'node:url';
const pluginData = {};
const scheduledNotifications = [];
globalThis.window = {
__VERSTAK_PLUGIN_REGISTRY__: {},
@@ -38,6 +39,14 @@ globalThis.window = {
pluginData[pluginId][name] = Object.assign({}, data || {});
return Promise.resolve('');
},
ReplacePluginNotifications: (pluginId, items) => {
scheduledNotifications.push({ pluginId, items });
return Promise.resolve(items[0]?.id === 'rejected' ? 'notification permission denied' : '');
},
ClearPluginNotifications: (pluginId) => {
scheduledNotifications.push({ pluginId, clear: true });
return Promise.resolve('');
},
},
},
},
@@ -116,6 +125,28 @@ if (stored.version !== 1 || stored.workspaceRootPath !== 'Project') {
throw new Error(`unexpected storage data: ${JSON.stringify(stored)}`);
}
if (!api.notifications || typeof api.notifications.replace !== 'function' || typeof api.notifications.clear !== 'function') {
throw new Error('api.notifications replace/clear contract is missing');
}
await api.notifications.replace([{ id: 'reminder-1', dueAt: '2026-07-14T10:00:00Z', title: 'Reminder' }]);
await api.notifications.clear();
if (scheduledNotifications.length !== 2
|| scheduledNotifications[0].pluginId !== 'verstak.files'
|| scheduledNotifications[0].items[0].id !== 'reminder-1'
|| scheduledNotifications[1].pluginId !== 'verstak.files'
|| !scheduledNotifications[1].clear) {
throw new Error(`unexpected notification calls: ${JSON.stringify(scheduledNotifications)}`);
}
let rejected = false;
try {
await api.notifications.replace([{ id: 'rejected', dueAt: '2026-07-14T10:00:00Z', title: 'Reminder' }]);
} catch (err) {
rejected = String(err.message || err).includes('[plugin:verstak.files] notifications.replace failed: notification permission denied');
}
if (!rejected) {
throw new Error('notification backend errors must be plugin-scoped rejections');
}
api.dispose();
if (localeListeners.size !== 0) {
throw new Error('api.i18n locale subscription was not disposed');