@goodandready/dsh-plugin-notify 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/lib/client.js +10 -11
- package/lib/index.js +96 -65
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.4.2] - 2026-10-10
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- **Request trust enforcement on HTTP/SSE routes** (#46): Route SSE and Push subscriptions through `isTrustedRequest` / `getRequestRejection`, ensuring origin and session checks with loopback fallback.
|
|
11
|
+
- **Webhook error logging diagnostic** (#49): Log human-readable channel name instead of truncated webhook URL on POST failure, preventing credential token leakage.
|
|
12
|
+
- **Multi-instance SSE client isolation** (#50): Isolate SSE client sets per plugin lifecycle context on dispose, avoiding cross-instance connection drops on reload.
|
|
13
|
+
- **Deterministic memory bound on turnStarts map** (#51): Enforce deterministic FIFO eviction bound (`maxSize = 100`) on `turnStarts` map under session bursts.
|
|
14
|
+
|
|
15
|
+
## [0.4.1] - 2026-10-10
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- **Settings registration namespace & Desktop crash protection** (#55): Use canonical `ENTRY_ID = 'plugin-notify'` instead of scoped package name in `settings.register`; wrap call with `try / catch` logging warning instead of throwing `TypeError` in DSH Desktop.
|
|
19
|
+
- **Settings form slot binding** (#54): Bind UI `settings.plugin.item` slot `key` to `ROW_ID` (`'plugin-notify'`), and prioritize `ROW_ID` in `configForms` candidates over legacy `NS`.
|
|
20
|
+
- **Dynamic settings observation** (#48): Make describe settings fallback reactive: observe `loader/volatile-update`, `settings/document-updated`, and `settings/updated` to dynamically refresh settings when edited in UI or external config.
|
|
21
|
+
- **Config unboxing guarantee** (#47): Guarantee `getConfig()` always unwraps volatile boxes via `unwrapConfig(config ?? {})`, preventing `TypeError` during `session/event` event unboxing.
|
|
22
|
+
- **DSH 0.2.0-rc.1 clean activation & volatile schema** (#42): Guarantee clean activation on DSH 0.2.0-rc.1 when `settings.register` is absent; confirm all user-facing fields in `Config` have `.volatile()` markers.
|
|
23
|
+
|
|
7
24
|
## [0.4.0] - 2026-10-05
|
|
8
25
|
|
|
9
26
|
### Added
|
package/lib/client.js
CHANGED
|
@@ -675,11 +675,10 @@ window.__ModuleLoader__.load({
|
|
|
675
675
|
}
|
|
676
676
|
}
|
|
677
677
|
|
|
678
|
-
//
|
|
679
|
-
//
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
() => (ctx && ctx.configForms ? [ctx.configForms.get(NS), ctx.configForms.get(ROW_ID)] : [undefined, undefined]),
|
|
678
|
+
// In DSH 0.2.x configForms is keyed strictly by entry id (ROW_ID = 'plugin-notify').
|
|
679
|
+
// Hold both candidates and prefer ROW_ID whenever it is ready or NS is unavailable.
|
|
680
|
+
const [scopeRow, scopeNS] = React.useMemo(
|
|
681
|
+
() => (ctx && ctx.configForms ? [ctx.configForms.get(ROW_ID), ctx.configForms.get(NS)] : [undefined, undefined]),
|
|
683
682
|
[ctx],
|
|
684
683
|
)
|
|
685
684
|
const useSnap = (scope) => React.useSyncExternalStore(
|
|
@@ -687,11 +686,11 @@ window.__ModuleLoader__.load({
|
|
|
687
686
|
React.useCallback(() => (scope ? scope.getSnapshot() : { status: 'loading' }), [scope]),
|
|
688
687
|
React.useCallback(() => ({ status: 'loading' }), []),
|
|
689
688
|
)
|
|
690
|
-
const snapNS = useSnap(scopeNS)
|
|
691
689
|
const snapRow = useSnap(scopeRow)
|
|
692
|
-
const
|
|
693
|
-
const
|
|
694
|
-
const
|
|
690
|
+
const snapNS = useSnap(scopeNS)
|
|
691
|
+
const useRow = scopeRow && (snapRow.status === 'ready' || snapNS.status !== 'ready')
|
|
692
|
+
const scope = useRow ? scopeRow : (scopeNS || scopeRow)
|
|
693
|
+
const snapshot = useRow ? snapRow : (scopeNS ? snapNS : snapRow)
|
|
695
694
|
|
|
696
695
|
React.useEffect(() => { ensureStyles() }, [])
|
|
697
696
|
|
|
@@ -905,7 +904,7 @@ window.__ModuleLoader__.load({
|
|
|
905
904
|
})
|
|
906
905
|
ctx.slots.inject('settings.plugin.item', () => {
|
|
907
906
|
ctx.slots.register(
|
|
908
|
-
{ name: 'settings.plugin.item', key:
|
|
907
|
+
{ name: 'settings.plugin.item', key: ROW_ID, locale: NS, order: 40, inject: () => ({ ctx }) },
|
|
909
908
|
(props) => React.createElement(NotifyCard, { ...props, ctx }),
|
|
910
909
|
)
|
|
911
910
|
})
|
|
@@ -916,7 +915,7 @@ window.__ModuleLoader__.load({
|
|
|
916
915
|
let currentSettings = {}
|
|
917
916
|
if (ctx.configForms) {
|
|
918
917
|
try {
|
|
919
|
-
const candidates = [ctx.configForms.get(
|
|
918
|
+
const candidates = [ctx.configForms.get(ROW_ID), ctx.configForms.get(NS)]
|
|
920
919
|
const ready = candidates.find((s) => s && s.getSnapshot && s.getSnapshot().status === 'ready')
|
|
921
920
|
const scope = ready || candidates[0]
|
|
922
921
|
if (scope && typeof scope.subscribe === 'function') {
|
package/lib/index.js
CHANGED
|
@@ -10,6 +10,8 @@ import { credentialRef } from '@deepseek-ai/dsh-credentials';
|
|
|
10
10
|
export const name = '@goodandready/dsh-plugin-notify';
|
|
11
11
|
/** Settings namespace shared with the Web settings card. */
|
|
12
12
|
export const NS = '@goodandready/dsh-plugin-notify';
|
|
13
|
+
/** Canonical entry id matching cordis.patch.yml */
|
|
14
|
+
export const ENTRY_ID = 'plugin-notify';
|
|
13
15
|
|
|
14
16
|
// Session firehose + credentials (webhook URL refs) + settings scope for the card + webServer for SSE events + connection for authentication.
|
|
15
17
|
export const inject = ['sessions', 'credentials', 'settings', 'webServer', 'connection'];
|
|
@@ -92,7 +94,7 @@ const warnedLegacyUrls = new Set();
|
|
|
92
94
|
* Purge turnStarts entries older than maxAgeMs (default 2 hours) to prevent
|
|
93
95
|
* memory leaks from abandoned or cancelled sessions.
|
|
94
96
|
*/
|
|
95
|
-
export function cleanupTurnStarts(maxAgeMs = 7200000, now = Date.now()) {
|
|
97
|
+
export function cleanupTurnStarts(maxAgeMs = 7200000, now = Date.now(), maxSize = 100) {
|
|
96
98
|
let deleted = 0;
|
|
97
99
|
for (const [sid, start] of turnStarts.entries()) {
|
|
98
100
|
if (now - start > maxAgeMs) {
|
|
@@ -100,6 +102,13 @@ export function cleanupTurnStarts(maxAgeMs = 7200000, now = Date.now()) {
|
|
|
100
102
|
deleted++;
|
|
101
103
|
}
|
|
102
104
|
}
|
|
105
|
+
if (typeof maxSize === 'number' && maxSize > 0 && turnStarts.size >= maxSize) {
|
|
106
|
+
for (const [sid] of turnStarts.entries()) {
|
|
107
|
+
if (turnStarts.size < maxSize) break;
|
|
108
|
+
turnStarts.delete(sid);
|
|
109
|
+
deleted++;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
103
112
|
return deleted;
|
|
104
113
|
}
|
|
105
114
|
|
|
@@ -166,17 +175,23 @@ function connectionOf(ctx) {
|
|
|
166
175
|
* Delegates to DSH connection service (Host/Origin fence + browser authentication session),
|
|
167
176
|
* with loopback-only fallback when connection service is absent (e.g. standalone tests).
|
|
168
177
|
*/
|
|
169
|
-
export function
|
|
170
|
-
if (!request || !request.headers) return
|
|
178
|
+
export function getRequestRejection(request, targetCtx) {
|
|
179
|
+
if (!request || !request.headers) return 403;
|
|
171
180
|
|
|
172
181
|
const conn = connectionOf(targetCtx);
|
|
173
182
|
if (conn && typeof conn.requestRejection === 'function') {
|
|
174
183
|
const rejection = conn.requestRejection(request);
|
|
175
|
-
|
|
184
|
+
if (rejection !== void 0) return rejection;
|
|
185
|
+
return void 0;
|
|
176
186
|
}
|
|
177
187
|
|
|
178
188
|
const remoteAddr = request.socket?.remoteAddress || request.connection?.remoteAddress;
|
|
179
|
-
|
|
189
|
+
if (!remoteAddr || !isLoopback(remoteAddr)) return 403;
|
|
190
|
+
return void 0;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function isTrustedRequest(request, targetCtx) {
|
|
194
|
+
return getRequestRejection(request, targetCtx) === void 0;
|
|
180
195
|
}
|
|
181
196
|
|
|
182
197
|
export function broadcastSse(eventData) {
|
|
@@ -194,7 +209,7 @@ export function broadcastSse(eventData) {
|
|
|
194
209
|
return sent;
|
|
195
210
|
}
|
|
196
211
|
|
|
197
|
-
export function handleSseConnection(req, res, targetCtx) {
|
|
212
|
+
export function handleSseConnection(req, res, targetCtx, instanceClients = null) {
|
|
198
213
|
if (req.method !== 'GET') {
|
|
199
214
|
res.statusCode = 405;
|
|
200
215
|
if (typeof res.setHeader === 'function') res.setHeader('allow', 'GET');
|
|
@@ -203,31 +218,16 @@ export function handleSseConnection(req, res, targetCtx) {
|
|
|
203
218
|
return;
|
|
204
219
|
}
|
|
205
220
|
|
|
206
|
-
const
|
|
207
|
-
if (
|
|
208
|
-
|
|
209
|
-
if (
|
|
210
|
-
res.
|
|
211
|
-
if (typeof res.writeHead === 'function') {
|
|
212
|
-
res.writeHead(rejection, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
213
|
-
}
|
|
214
|
-
if (typeof res.end === 'function') {
|
|
215
|
-
res.end(rejection === 401 ? 'Unauthorized' : 'Forbidden');
|
|
216
|
-
}
|
|
217
|
-
return;
|
|
221
|
+
const rejection = getRequestRejection(req, targetCtx);
|
|
222
|
+
if (rejection !== void 0) {
|
|
223
|
+
res.statusCode = rejection;
|
|
224
|
+
if (typeof res.writeHead === 'function') {
|
|
225
|
+
res.writeHead(rejection, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
218
226
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (!remoteAddr || !isLoopback(remoteAddr)) {
|
|
222
|
-
res.statusCode = 403;
|
|
223
|
-
if (typeof res.writeHead === 'function') {
|
|
224
|
-
res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
225
|
-
}
|
|
226
|
-
if (typeof res.end === 'function') {
|
|
227
|
-
res.end('Forbidden');
|
|
228
|
-
}
|
|
229
|
-
return;
|
|
227
|
+
if (typeof res.end === 'function') {
|
|
228
|
+
res.end(rejection === 401 ? 'Unauthorized' : 'Forbidden');
|
|
230
229
|
}
|
|
230
|
+
return;
|
|
231
231
|
}
|
|
232
232
|
|
|
233
233
|
if (typeof res.writeHead === 'function') {
|
|
@@ -241,9 +241,15 @@ export function handleSseConnection(req, res, targetCtx) {
|
|
|
241
241
|
res.write(': connected\n\n');
|
|
242
242
|
}
|
|
243
243
|
sseClients.add(res);
|
|
244
|
+
if (instanceClients) {
|
|
245
|
+
instanceClients.add(res);
|
|
246
|
+
}
|
|
244
247
|
|
|
245
248
|
const onEnd = () => {
|
|
246
249
|
sseClients.delete(res);
|
|
250
|
+
if (instanceClients) {
|
|
251
|
+
instanceClients.delete(res);
|
|
252
|
+
}
|
|
247
253
|
};
|
|
248
254
|
if (typeof req.on === 'function') req.on('close', onEnd);
|
|
249
255
|
if (typeof res.on === 'function') {
|
|
@@ -252,8 +258,9 @@ export function handleSseConnection(req, res, targetCtx) {
|
|
|
252
258
|
}
|
|
253
259
|
}
|
|
254
260
|
|
|
255
|
-
export function closeAllSseClients() {
|
|
256
|
-
|
|
261
|
+
export function closeAllSseClients(clientsToClose = sseClients) {
|
|
262
|
+
const list = Array.from(clientsToClose);
|
|
263
|
+
for (const res of list) {
|
|
257
264
|
try {
|
|
258
265
|
if (!res.writableEnded) {
|
|
259
266
|
if (typeof res.write === 'function' && res.headersSent) {
|
|
@@ -271,9 +278,16 @@ export function closeAllSseClients() {
|
|
|
271
278
|
// ignore close error
|
|
272
279
|
} finally {
|
|
273
280
|
sseClients.delete(res);
|
|
281
|
+
if (clientsToClose !== sseClients) {
|
|
282
|
+
clientsToClose.delete(res);
|
|
283
|
+
}
|
|
274
284
|
}
|
|
275
285
|
}
|
|
276
|
-
sseClients
|
|
286
|
+
if (clientsToClose !== sseClients) {
|
|
287
|
+
clientsToClose.clear();
|
|
288
|
+
} else {
|
|
289
|
+
sseClients.clear();
|
|
290
|
+
}
|
|
277
291
|
}
|
|
278
292
|
|
|
279
293
|
// ---------------------------------------------------------------------------
|
|
@@ -545,18 +559,9 @@ function writeText(res, status, text, headers = {}) {
|
|
|
545
559
|
|
|
546
560
|
/** Shared auth for push routes: same fence as the SSE stream. Returns true when accepted. */
|
|
547
561
|
export function authorizePushRequest(req, res, targetCtx) {
|
|
548
|
-
const
|
|
549
|
-
if (
|
|
550
|
-
|
|
551
|
-
if (rejection !== void 0) {
|
|
552
|
-
writeText(res, rejection, rejection === 401 ? 'Unauthorized' : 'Forbidden');
|
|
553
|
-
return false;
|
|
554
|
-
}
|
|
555
|
-
return true;
|
|
556
|
-
}
|
|
557
|
-
const remoteAddr = req.socket?.remoteAddress || req.connection?.remoteAddress;
|
|
558
|
-
if (!remoteAddr || !isLoopback(remoteAddr)) {
|
|
559
|
-
writeText(res, 403, 'Forbidden');
|
|
562
|
+
const rejection = getRequestRejection(req, targetCtx);
|
|
563
|
+
if (rejection !== void 0) {
|
|
564
|
+
writeText(res, rejection, rejection === 401 ? 'Unauthorized' : 'Forbidden');
|
|
560
565
|
return false;
|
|
561
566
|
}
|
|
562
567
|
return true;
|
|
@@ -647,6 +652,7 @@ export async function handlePushTest(req, res, targetCtx, getConfig) {
|
|
|
647
652
|
|
|
648
653
|
function mountWebServer(targetCtx, hostCtx, getConfig) {
|
|
649
654
|
if (!targetCtx?.webServer?.register) return () => {};
|
|
655
|
+
const instanceClients = new Set();
|
|
650
656
|
// Async route wrapper: an unexpected rejection becomes a 500 instead of an
|
|
651
657
|
// unhandled rejection; authorized handlers have already written their response.
|
|
652
658
|
const asyncRoute = (handler) => (req, res) => {
|
|
@@ -658,7 +664,7 @@ function mountWebServer(targetCtx, hostCtx, getConfig) {
|
|
|
658
664
|
{
|
|
659
665
|
kind: 'exact',
|
|
660
666
|
path: '/dsh-plugin-notify/events',
|
|
661
|
-
handler: (req, res) => handleSseConnection(req, res, targetCtx),
|
|
667
|
+
handler: (req, res) => handleSseConnection(req, res, targetCtx, instanceClients),
|
|
662
668
|
},
|
|
663
669
|
{
|
|
664
670
|
kind: 'exact',
|
|
@@ -714,7 +720,7 @@ function mountWebServer(targetCtx, hostCtx, getConfig) {
|
|
|
714
720
|
}
|
|
715
721
|
unregister = null;
|
|
716
722
|
}
|
|
717
|
-
closeAllSseClients();
|
|
723
|
+
closeAllSseClients(instanceClients);
|
|
718
724
|
};
|
|
719
725
|
|
|
720
726
|
if (typeof targetCtx.effect === 'function') {
|
|
@@ -734,7 +740,7 @@ function mountWebServer(targetCtx, hostCtx, getConfig) {
|
|
|
734
740
|
}
|
|
735
741
|
|
|
736
742
|
export function apply(ctx, config = {}) {
|
|
737
|
-
let getConfig = () => config ?? {};
|
|
743
|
+
let getConfig = () => unwrapConfig(config ?? {});
|
|
738
744
|
|
|
739
745
|
let webServerMounted = false;
|
|
740
746
|
let unmountWebServer = null;
|
|
@@ -753,7 +759,6 @@ export function apply(ctx, config = {}) {
|
|
|
753
759
|
try { unmountWebServer(); } catch { /* ignore */ }
|
|
754
760
|
unmountWebServer = null;
|
|
755
761
|
}
|
|
756
|
-
closeAllSseClients();
|
|
757
762
|
turnStarts.clear();
|
|
758
763
|
webServerMounted = false;
|
|
759
764
|
};
|
|
@@ -768,22 +773,48 @@ export function apply(ctx, config = {}) {
|
|
|
768
773
|
if (typeof ctx.inject === 'function') {
|
|
769
774
|
ctx.inject(['settings'], (sctx) => {
|
|
770
775
|
if (typeof sctx.settings?.register === 'function') {
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
776
|
+
try {
|
|
777
|
+
const scope = sctx.settings.register(ENTRY_ID, Config, { base: config ?? {} });
|
|
778
|
+
getConfig = () => (scope ? unwrapConfig(scope.get() ?? config ?? {}) : unwrapConfig(config ?? {}));
|
|
779
|
+
return;
|
|
780
|
+
} catch (err) {
|
|
781
|
+
ctx?.logger?.warn?.(`[plugin-notify] settings.register('${ENTRY_ID}') failed, falling back to describe: ${String(err)}`);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
let currentRow = (() => { try { return sctx.settings?.describe?.().find((r) => r && (r.ns === ENTRY_ID || r.ns === NS)); } catch { return null; } })();
|
|
785
|
+
const refreshSettings = () => {
|
|
786
|
+
try {
|
|
787
|
+
const next = sctx.settings?.describe?.().find((r) => r && (r.ns === ENTRY_ID || r.ns === NS));
|
|
788
|
+
if (next) currentRow = next;
|
|
789
|
+
} catch {
|
|
790
|
+
// best effort
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
if (typeof ctx.on === 'function') {
|
|
794
|
+
ctx.on('settings/document-updated', refreshSettings);
|
|
795
|
+
ctx.on('loader/volatile-update', refreshSettings);
|
|
796
|
+
ctx.on('settings/updated', refreshSettings);
|
|
797
|
+
}
|
|
798
|
+
if (typeof sctx.settings?.on === 'function') {
|
|
799
|
+
try { sctx.settings.on('update', refreshSettings); } catch { /* ignore */ }
|
|
774
800
|
}
|
|
775
|
-
const row = (() => { try { return sctx.settings?.describe?.().find((r) => r && r.ns === NS); } catch { return null; } })();
|
|
776
801
|
const scope = {
|
|
777
|
-
get: () =>
|
|
802
|
+
get: () => {
|
|
803
|
+
refreshSettings();
|
|
804
|
+
return currentRow?.value ?? config ?? {};
|
|
805
|
+
},
|
|
778
806
|
replace: async (next) => {
|
|
779
807
|
try {
|
|
780
|
-
|
|
808
|
+
const targetNs = sctx.settings?.describe?.().some((r) => r && r.ns === ENTRY_ID) ? ENTRY_ID : NS;
|
|
809
|
+
const res = await sctx.settings?.replace?.(targetNs, next, currentRow?.revision);
|
|
810
|
+
refreshSettings();
|
|
811
|
+
return res;
|
|
781
812
|
} catch (err) {
|
|
782
813
|
ctx?.logger?.warn?.(`[plugin-notify] settings replace failed: ${String(err)}`);
|
|
783
814
|
}
|
|
784
815
|
},
|
|
785
816
|
};
|
|
786
|
-
getConfig = () => scope.get() ?? config ?? {};
|
|
817
|
+
getConfig = () => (scope ? unwrapConfig(scope.get() ?? config ?? {}) : unwrapConfig(config ?? {}));
|
|
787
818
|
});
|
|
788
819
|
ctx.inject(['webServer'], (wctx) => tryMountWebServer(wctx))
|
|
789
820
|
}
|
|
@@ -842,8 +873,8 @@ export function apply(ctx, config = {}) {
|
|
|
842
873
|
const excludeSessionPrefixes = cfg.excludeSessionPrefixes ?? [];
|
|
843
874
|
|
|
844
875
|
if (event.type === 'turn/start') {
|
|
845
|
-
if (turnStarts.size
|
|
846
|
-
cleanupTurnStarts();
|
|
876
|
+
if (turnStarts.size >= 100) {
|
|
877
|
+
cleanupTurnStarts(7200000, Date.now(), 100);
|
|
847
878
|
}
|
|
848
879
|
turnStarts.set(String(session.id), Date.now());
|
|
849
880
|
return;
|
|
@@ -895,28 +926,28 @@ function send(ctx, n, webhooks, timeoutMs, local, includeSession, includeDuratio
|
|
|
895
926
|
const signal = AbortSignal.timeout(timeoutMs);
|
|
896
927
|
const channels = Object.keys(webhooks);
|
|
897
928
|
(ctx?.logger?.debug ?? ctx?.logger?.info)?.(`[plugin-notify] ${n.kind} · ${n.title} · session ${n.sessionId} · channels ${channels.join(',') || 'none'} · local ${local}`);
|
|
898
|
-
const post = (url, body) => {
|
|
929
|
+
const post = (channel, url, body) => {
|
|
899
930
|
fetch(url, {
|
|
900
931
|
method: 'POST',
|
|
901
932
|
headers: { 'content-type': 'application/json' },
|
|
902
933
|
body: JSON.stringify(body),
|
|
903
934
|
signal,
|
|
904
935
|
}).catch(error => {
|
|
905
|
-
ctx?.logger?.warn?.(`[plugin-notify] webhook POST failed
|
|
936
|
+
ctx?.logger?.warn?.(`[plugin-notify] ${channel} webhook POST failed: ${String(error)}`);
|
|
906
937
|
});
|
|
907
938
|
};
|
|
908
939
|
if (webhooks.feishu)
|
|
909
|
-
post(webhooks.feishu, { msg_type: 'text', content: { text } });
|
|
940
|
+
post('feishu', webhooks.feishu, { msg_type: 'text', content: { text } });
|
|
910
941
|
if (webhooks.wecom)
|
|
911
|
-
post(webhooks.wecom, { msgtype: 'text', text: { content: text } });
|
|
942
|
+
post('wecom', webhooks.wecom, { msgtype: 'text', text: { content: text } });
|
|
912
943
|
if (webhooks.dingtalk)
|
|
913
|
-
post(webhooks.dingtalk, { msgtype: 'text', text: { content: text } });
|
|
944
|
+
post('dingtalk', webhooks.dingtalk, { msgtype: 'text', text: { content: text } });
|
|
914
945
|
if (webhooks.slack)
|
|
915
|
-
post(webhooks.slack, { text });
|
|
946
|
+
post('slack', webhooks.slack, { text });
|
|
916
947
|
if (webhooks.discord)
|
|
917
|
-
post(webhooks.discord, { content: text });
|
|
948
|
+
post('discord', webhooks.discord, { content: text });
|
|
918
949
|
if (webhooks.custom) {
|
|
919
|
-
post(webhooks.custom, {
|
|
950
|
+
post('custom', webhooks.custom, {
|
|
920
951
|
text,
|
|
921
952
|
kind: n.kind,
|
|
922
953
|
title: n.title,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-plugin-notify",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "DSH plugin: audio chimes, cross-session toasts, desktop push, IM webhooks, and web push (PWA) for turn completion, errors, and approvals.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|