@goodandready/dsh-messenger-gateway 0.3.13 → 0.3.18
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/README.md +6 -0
- package/lib/adapters/telegram.js +46 -3
- package/lib/client.js +1307 -629
- package/lib/content-guard.js +14 -0
- package/lib/gateway.js +81 -45
- package/lib/index.js +49 -15
- package/lib/models.js +4 -3
- package/lib/outbound.js +2 -1
- package/lib/pairing.js +19 -5
- package/lib/photos.js +2 -1
- package/lib/scheduler.js +5 -5
- package/lib/session-ops.js +14 -4
- package/lib/storage-atomic.js +21 -0
- package/lib/stream.js +39 -2
- package/lib/telegram-errors.js +51 -41
- package/lib/voice-prefs.js +19 -4
- package/package.json +2 -5
package/lib/client.js
CHANGED
|
@@ -1,629 +1,1307 @@
|
|
|
1
|
-
// dsh-messenger-gateway — browser (client) half.
|
|
2
|
-
// Settings card on Plugins → Plugin settings tab (settings.plugin.item).
|
|
3
|
-
// Config managed via reactive ctx.settingsScope snapshot.
|
|
4
|
-
|
|
5
|
-
window.__ModuleLoader__.load({
|
|
6
|
-
id: '@goodandready/dsh-messenger-gateway',
|
|
7
|
-
factory: (require) => {
|
|
8
|
-
var module = { exports: {} }
|
|
9
|
-
var exports = module.exports
|
|
10
|
-
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
|
|
11
|
-
const React = require('react')
|
|
12
|
-
|
|
13
|
-
const NS = 'dsh-messenger-gateway'
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
'.
|
|
19
|
-
'.
|
|
20
|
-
'.
|
|
21
|
-
'.
|
|
22
|
-
'.
|
|
23
|
-
'.
|
|
24
|
-
'.
|
|
25
|
-
'.
|
|
26
|
-
'.
|
|
27
|
-
'.
|
|
28
|
-
'.
|
|
29
|
-
'.
|
|
30
|
-
'.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
'.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
title: '
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
React.
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
),
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
),
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
),
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
),
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
)
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
)
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
),
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
)
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
)
|
|
579
|
-
)
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
)
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
React.createElement(
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
1
|
+
// dsh-messenger-gateway — browser (client) half.
|
|
2
|
+
// Settings card on Plugins → Plugin settings tab (settings.plugin.item).
|
|
3
|
+
// Config managed via reactive ctx.settingsScope snapshot.
|
|
4
|
+
|
|
5
|
+
window.__ModuleLoader__.load({
|
|
6
|
+
id: '@goodandready/dsh-messenger-gateway',
|
|
7
|
+
factory: (require) => {
|
|
8
|
+
var module = { exports: {} }
|
|
9
|
+
var exports = module.exports
|
|
10
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
|
|
11
|
+
const React = require('react')
|
|
12
|
+
|
|
13
|
+
const NS = 'dsh-messenger-gateway'
|
|
14
|
+
const ROUTE_PREFIX = '/dsh-messenger-gateway'
|
|
15
|
+
|
|
16
|
+
const en = {
|
|
17
|
+
title: 'Messenger gateway',
|
|
18
|
+
description: 'Telegram bot: text, voice, photos, documents, and interactive asks.',
|
|
19
|
+
'settings.loading': 'Loading Messenger gateway settings…',
|
|
20
|
+
'settings.retry': 'Retry',
|
|
21
|
+
'settings.unavailable': 'Settings unavailable (host namespace is not ready).',
|
|
22
|
+
'header.title': 'Telegram & Multi-Messenger Gateway',
|
|
23
|
+
'header.sub': 'Bridge between DeepSeek Harness and Telegram: sessions, steer, forum topics, inline buttons, and optional voice notes.',
|
|
24
|
+
'badge.online': 'Telegram online (@{username})',
|
|
25
|
+
'badge.running': 'Running',
|
|
26
|
+
'badge.stopped': 'Stopped',
|
|
27
|
+
'badge.token_ok': 'Token ✓',
|
|
28
|
+
'badge.token_missing': 'Token missing',
|
|
29
|
+
'badge.pairing_count': 'Pairing: {count}',
|
|
30
|
+
'badge.transport': 'Transport: {mode}',
|
|
31
|
+
save: 'Save Changes',
|
|
32
|
+
saved: 'Settings saved successfully',
|
|
33
|
+
saving: 'Saving…',
|
|
34
|
+
showAdvanced: '⚙️ Advanced connection & polling settings',
|
|
35
|
+
hideAdvanced: 'Hide advanced settings',
|
|
36
|
+
telegram: 'Telegram Bot Configuration',
|
|
37
|
+
telegramSub: 'Main bot credentials, whitelist, and message delivery settings.',
|
|
38
|
+
agent: 'Agent & Reasoning Execution',
|
|
39
|
+
agentSub: 'Model routing, instruction overrides, and turn timeout control.',
|
|
40
|
+
media: 'Media & File Limits',
|
|
41
|
+
mediaSub: 'Document extraction and image upload limits.',
|
|
42
|
+
tts: 'Voice Replies (dsh-tts)',
|
|
43
|
+
ttsSub: 'Synthesize spoken replies for Telegram voice messages.',
|
|
44
|
+
advanced: 'Connection & Polling Internals',
|
|
45
|
+
botToken: 'Telegram Bot Token',
|
|
46
|
+
botTokenHint: 'Obtained via @BotFather. Leave empty to keep existing configured token.',
|
|
47
|
+
allowedIds: 'Allowed Telegram User IDs',
|
|
48
|
+
allowedIdsHint: 'Comma-separated list of user IDs. Leave empty to allow all users (pairing requests enabled).',
|
|
49
|
+
textFormat: 'Text Format Rendering',
|
|
50
|
+
textFormatHtml: 'Markdown → Telegram HTML (recommended)',
|
|
51
|
+
textFormatPlain: 'Plain text (no formatting)',
|
|
52
|
+
disableTelegram: 'Disable Telegram Bot',
|
|
53
|
+
enableTelegram: 'Enable Telegram Bot',
|
|
54
|
+
provider: 'Provider Override',
|
|
55
|
+
providerHint: 'Leave empty to use DSH default provider',
|
|
56
|
+
model: 'Model Override',
|
|
57
|
+
modelHint: 'Leave empty to use DSH default model',
|
|
58
|
+
photoOnly: 'Photo without Caption Behavior',
|
|
59
|
+
photoPrompt: 'Wait for question (recommended)',
|
|
60
|
+
photoRun: 'Reply immediately',
|
|
61
|
+
instruction: 'Extra Agent Instruction',
|
|
62
|
+
instructionHint: 'Prepended to each inbound messenger message',
|
|
63
|
+
instructionPlaceholder: 'Empty = built-in relay instruction (Russian, concise)',
|
|
64
|
+
maxMessageLength: 'Max Reply Length (characters)',
|
|
65
|
+
turnTimeout: 'Agent Turn Timeout (seconds)',
|
|
66
|
+
maxDocMb: 'Max Document Size (MB)',
|
|
67
|
+
maxImageMb: 'Max Photo Size (MB)',
|
|
68
|
+
maxTextKb: 'Max Extracted Text from Document (KB)',
|
|
69
|
+
ttsEnable: 'Enable Voice Replies (requires dsh-tts)',
|
|
70
|
+
ttsMaxChars: 'Max Spoken Characters per Voice Note',
|
|
71
|
+
pollTimeout: 'Long Poll Timeout (seconds)',
|
|
72
|
+
pollInterval: 'Long Poll Interval (ms)',
|
|
73
|
+
idleTimeout: 'Idle Chat Timeout (seconds)',
|
|
74
|
+
pairing: 'Pairing Requests & Authorization',
|
|
75
|
+
pairingSub: 'Authorize new users when whitelist is restricted.',
|
|
76
|
+
pairingEmpty: 'No pending pairing codes',
|
|
77
|
+
pairingApprove: 'Approve',
|
|
78
|
+
pairingReject: 'Reject',
|
|
79
|
+
pairingRefresh: 'Refresh List',
|
|
80
|
+
groupsEnable: 'Respond in Telegram Groups & Supergroups',
|
|
81
|
+
groupMention: 'Require @mention, reply, or /command in groups',
|
|
82
|
+
reactionsEnable: 'React with 👀 while processing turn',
|
|
83
|
+
progressEnable: 'Progress status message (Thinking → Delete → Final)',
|
|
84
|
+
statusIndicator: 'Bot Status Description Indicator (Online/Offline)',
|
|
85
|
+
statusIndicatorHint: 'Sets bot short description via Telegram API (visible to all users).',
|
|
86
|
+
transport: 'Telegram Transport Mode',
|
|
87
|
+
transportPoll: 'Long poll (getUpdates)',
|
|
88
|
+
transportWebhook: 'Webhook (HTTPS)',
|
|
89
|
+
webhookUrl: 'Public Webhook HTTPS URL',
|
|
90
|
+
webhookUrlHint: 'Public endpoint where Telegram sends updates',
|
|
91
|
+
webhookSecret: 'Webhook Secret Token',
|
|
92
|
+
webhookSecretHint: 'Leave empty to keep current configured secret',
|
|
93
|
+
sessionScope: 'Group Session Scope',
|
|
94
|
+
sessionScopeChat: 'Shared per chat/topic',
|
|
95
|
+
sessionScopeUser: 'Per-user session (recommended)',
|
|
96
|
+
voiceMode: 'Voice Reply Mode',
|
|
97
|
+
voiceModeMirror: 'Mirror inbound voice (reply voice only if input was voice)',
|
|
98
|
+
voiceModeAlways: 'Always synthesize voice note',
|
|
99
|
+
voiceModeOff: 'Off (unless explicitly toggled via /voice)',
|
|
100
|
+
notifyBridge: 'Notify Bridge → Telegram Home',
|
|
101
|
+
notifyBridgeHint: 'Forward task_done/error events from web sessions into designated Telegram home',
|
|
102
|
+
notifyHome: 'Notify Target Home Name',
|
|
103
|
+
quickActions: 'Quick Actions Keyboard (/new, /stop, /voice, /status)',
|
|
104
|
+
artifactPreviews: 'Render diagram and table previews in Telegram',
|
|
105
|
+
'diag.title': '⚡ Diagnostics & Telegram Smoke Test',
|
|
106
|
+
'diag.desc': 'Test connection to Telegram Bot API, measure network latency, and verify bot identity.',
|
|
107
|
+
'diag.smoke_btn': 'Run Telegram Smoke Test (Ping)',
|
|
108
|
+
'diag.smoke_testing': 'Testing Telegram API…',
|
|
109
|
+
'diag.smoke_ok': 'Telegram API OK! Latency: {latency} ms (@{username}, ID: {id})',
|
|
110
|
+
'diag.smoke_err': 'Smoke test failed: {error}',
|
|
111
|
+
'stats.title': '📊 Gateway Telemetry & Session Counters',
|
|
112
|
+
'stats.desc': 'Real-time counters from the running gateway process.',
|
|
113
|
+
'stats.sent': 'Messages Delivered',
|
|
114
|
+
'stats.errors': 'Delivery Errors',
|
|
115
|
+
'stats.uptime': 'Gateway Uptime',
|
|
116
|
+
'stats.active_chats': 'Active Chats in Memory',
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const ru = {
|
|
120
|
+
title: 'Messenger gateway',
|
|
121
|
+
description: 'Telegram-бот: текст, голос, фото, документы и интерактивные опросы.',
|
|
122
|
+
'settings.loading': 'Загрузка настроек Messenger gateway…',
|
|
123
|
+
'settings.retry': 'Повторить попытку',
|
|
124
|
+
'settings.unavailable': 'Настройки недоступны (пространство хоста ещё не готово).',
|
|
125
|
+
'header.title': 'Шлюз Telegram и мессенджеров',
|
|
126
|
+
'header.sub': 'Мост между DeepSeek Harness и Telegram: сессии, steer, топики форума, инлайн-кнопки и голосовые ответы.',
|
|
127
|
+
'badge.online': 'Telegram онлайн (@{username})',
|
|
128
|
+
'badge.running': 'Работает',
|
|
129
|
+
'badge.stopped': 'Остановлен',
|
|
130
|
+
'badge.token_ok': 'Токен ✓',
|
|
131
|
+
'badge.token_missing': 'Токен отсутствует',
|
|
132
|
+
'badge.pairing_count': 'Сопряжение: {count}',
|
|
133
|
+
'badge.transport': 'Транспорт: {mode}',
|
|
134
|
+
save: 'Сохранить изменения',
|
|
135
|
+
saved: 'Настройки успешно сохранены',
|
|
136
|
+
saving: 'Сохранение…',
|
|
137
|
+
showAdvanced: '⚙️ Расширенные настройки подключения и polling',
|
|
138
|
+
hideAdvanced: 'Скрыть расширенные настройки',
|
|
139
|
+
telegram: 'Настройки бота Telegram',
|
|
140
|
+
telegramSub: 'Ключи доступа, белый список пользователей и параметры доставки сообщений.',
|
|
141
|
+
agent: 'Поведение агента и рассуждения',
|
|
142
|
+
agentSub: 'Маршрутизация модели, дополнительные инструкции и таймаут хода.',
|
|
143
|
+
media: 'Ограничения медиа и документов',
|
|
144
|
+
mediaSub: 'Лимиты на извлечение текста и размер загружаемых файлов.',
|
|
145
|
+
tts: 'Голосовые ответы (dsh-tts)',
|
|
146
|
+
ttsSub: 'Синтез аудиоответов на голосовые сообщения в чате.',
|
|
147
|
+
advanced: 'Параметры соединения и polling',
|
|
148
|
+
botToken: 'Токен бота Telegram',
|
|
149
|
+
botTokenHint: 'Получен от @BotFather. Оставьте пустым, чтобы сохранить текущий.',
|
|
150
|
+
allowedIds: 'Разрешённые ID пользователей',
|
|
151
|
+
allowedIdsHint: 'ID пользователей через запятую. Пусто = доступно всем (включено сопряжение по кодам).',
|
|
152
|
+
textFormat: 'Форматирование текста',
|
|
153
|
+
textFormatHtml: 'Markdown → Telegram HTML (рекомендуется)',
|
|
154
|
+
textFormatPlain: 'Простой текст (без форматирования)',
|
|
155
|
+
disableTelegram: 'Выключить Telegram-бота',
|
|
156
|
+
enableTelegram: 'Включить Telegram-бота',
|
|
157
|
+
provider: 'Провайдер по умолчанию',
|
|
158
|
+
providerHint: 'Пусто = провайдер по умолчанию DSH',
|
|
159
|
+
model: 'Модель по умолчанию',
|
|
160
|
+
modelHint: 'Пусто = модель по умолчанию DSH',
|
|
161
|
+
photoOnly: 'Поведение фото без подписи',
|
|
162
|
+
photoPrompt: 'Ждать вопроса (рекомендуется)',
|
|
163
|
+
photoRun: 'Отвечать сразу',
|
|
164
|
+
instruction: 'Дополнительная инструкция агенту',
|
|
165
|
+
instructionHint: 'Добавляется к каждому входящему сообщению из мессенджера',
|
|
166
|
+
instructionPlaceholder: 'Пусто = встроенная инструкция (русский язык, лаконичный ответ)',
|
|
167
|
+
maxMessageLength: 'Макс. длина ответа (символов)',
|
|
168
|
+
turnTimeout: 'Таймаут хода агента (сек)',
|
|
169
|
+
maxDocMb: 'Макс. размер документа (МБ)',
|
|
170
|
+
maxImageMb: 'Макс. размер фото (МБ)',
|
|
171
|
+
maxTextKb: 'Макс. объём текста из документа (КБ)',
|
|
172
|
+
ttsEnable: 'Включить голосовые ответы (требуется dsh-tts)',
|
|
173
|
+
ttsMaxChars: 'Макс. символов для озвучивания',
|
|
174
|
+
pollTimeout: 'Таймаут Long Poll (сек)',
|
|
175
|
+
pollInterval: 'Интервал Long Poll (мс)',
|
|
176
|
+
idleTimeout: 'Таймаут неактивного чата (сек)',
|
|
177
|
+
pairing: 'Запросы сопряжения и авторизация',
|
|
178
|
+
pairingSub: 'Подтверждение доступа новых пользователей при ограниченном списке.',
|
|
179
|
+
pairingEmpty: 'Нет ожидающих кодов сопряжения',
|
|
180
|
+
pairingApprove: 'Одобрить',
|
|
181
|
+
pairingReject: 'Отклонить',
|
|
182
|
+
pairingRefresh: 'Обновить список',
|
|
183
|
+
groupsEnable: 'Отвечать в группах и супергруппах',
|
|
184
|
+
groupMention: 'В группах отвечать только на @упоминание, reply или /команду',
|
|
185
|
+
reactionsEnable: 'Реакция 👀 пока агент думает',
|
|
186
|
+
progressEnable: 'Индикатор прогресса (Думаю → Удалить → Ответ)',
|
|
187
|
+
statusIndicator: 'Индикатор статуса (Online/Offline)',
|
|
188
|
+
statusIndicatorHint: 'Меняет краткое описание бота в Telegram (видно всем).',
|
|
189
|
+
transport: 'Режим транспорта',
|
|
190
|
+
transportPoll: 'Long poll (getUpdates)',
|
|
191
|
+
transportWebhook: 'Webhook (HTTPS)',
|
|
192
|
+
webhookUrl: 'Публичный HTTPS URL webhook',
|
|
193
|
+
webhookUrlHint: 'Публичный адрес, куда Telegram доставляет входящие обновления',
|
|
194
|
+
webhookSecret: 'Секретный токен webhook',
|
|
195
|
+
webhookSecretHint: 'Оставьте пустым, чтобы не менять текущий секрет',
|
|
196
|
+
sessionScope: 'Сессии в группах',
|
|
197
|
+
sessionScopeChat: 'Общая сессия на чат/топик',
|
|
198
|
+
sessionScopeUser: 'Индивидуальная на пользователя (рекомендуется)',
|
|
199
|
+
voiceMode: 'Режим голосовых ответов',
|
|
200
|
+
voiceModeMirror: 'Как входящее (голос на голос)',
|
|
201
|
+
voiceModeAlways: 'Всегда отвечать голосовым',
|
|
202
|
+
voiceModeOff: 'Выключено (если не включено через /voice)',
|
|
203
|
+
notifyBridge: 'Мост уведомлений → Telegram Home',
|
|
204
|
+
notifyBridgeHint: 'Пересылка событий task_done/error из web-сессий в указанный канал/топик',
|
|
205
|
+
notifyHome: 'Имя целевого канала (Home)',
|
|
206
|
+
quickActions: 'Клавиатура быстрых действий (/new, /stop, /voice, /status)',
|
|
207
|
+
artifactPreviews: 'Превью диаграмм и таблиц в Telegram',
|
|
208
|
+
'diag.title': '⚡ Диагностика и проверка связи (Smoke test)',
|
|
209
|
+
'diag.desc': 'Проверка подключения к Telegram Bot API, замер сетевой задержки и валидация имени бота.',
|
|
210
|
+
'diag.smoke_btn': 'Проверить соединение с Telegram (Ping)',
|
|
211
|
+
'diag.smoke_testing': 'Проверка связи с Telegram API…',
|
|
212
|
+
'diag.smoke_ok': 'Связь с Telegram API установлена! Задержка: {latency} мс (@{username}, ID: {id})',
|
|
213
|
+
'diag.smoke_err': 'Ошибка проверки связи: {error}',
|
|
214
|
+
'stats.title': '📊 Телеметрия шлюза и счётчики',
|
|
215
|
+
'stats.desc': 'Статистика реального времени работающего процесса шлюза.',
|
|
216
|
+
'stats.sent': 'Сообщений отправлено',
|
|
217
|
+
'stats.errors': 'Ошибок доставки',
|
|
218
|
+
'stats.uptime': 'Время работы шлюза',
|
|
219
|
+
'stats.active_chats': 'Активных чатов в памяти',
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function useActiveLocale(ctx) {
|
|
223
|
+
const loc = ctx?.get?.('locale') || ctx?.locale
|
|
224
|
+
return React.useSyncExternalStore(
|
|
225
|
+
React.useMemo(() => (cb) => (loc ? loc.subscribe(cb) : () => {}), [loc]),
|
|
226
|
+
React.useCallback(() => {
|
|
227
|
+
if (loc) {
|
|
228
|
+
const active = loc.getSnapshot?.()?.active
|
|
229
|
+
if (typeof active === 'string' && active) return active
|
|
230
|
+
}
|
|
231
|
+
return typeof navigator !== 'undefined' ? String(navigator.language || '').slice(0, 2) : 'en'
|
|
232
|
+
}, [loc]),
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function makeT(locale) {
|
|
237
|
+
const DICT = String(locale || '').startsWith('ru') ? ru : en
|
|
238
|
+
return (key, params) => {
|
|
239
|
+
let text = DICT[key] || en[key] || key
|
|
240
|
+
if (params && typeof params === 'object') {
|
|
241
|
+
for (const k of Object.keys(params)) {
|
|
242
|
+
text = text.replace(new RegExp('\\{' + k + '\\}', 'g'), String(params[k]))
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return text
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function ensureCss() {
|
|
250
|
+
if (typeof document === 'undefined') return
|
|
251
|
+
const id = 'dsh-messenger-gateway-full-css'
|
|
252
|
+
if (document.getElementById(id)) return
|
|
253
|
+
const style = document.createElement('style')
|
|
254
|
+
style.id = id
|
|
255
|
+
style.dataset.dshPlugin = NS
|
|
256
|
+
style.textContent = `
|
|
257
|
+
.msgw-page{display:flex;flex-direction:column;gap:16px;color:var(--dsw-alias-label-primary);font-family:inherit}
|
|
258
|
+
.msgw-header{display:flex;flex-direction:column;gap:4px;padding-bottom:12px;border-bottom:1px solid var(--dsw-alias-border-l2)}
|
|
259
|
+
.msgw-page-title{font-size:18px;font-weight:700;display:flex;align-items:center;gap:10px;flex-wrap:wrap;color:var(--dsw-alias-label-primary)}
|
|
260
|
+
.msgw-page-sub{font-size:13px;color:var(--dsw-alias-label-secondary);line-height:1.4}
|
|
261
|
+
|
|
262
|
+
.msgw-badge{font-size:11px;font-weight:600;padding:2px 8px;border-radius:12px;display:inline-flex;align-items:center;gap:4px;white-space:nowrap}
|
|
263
|
+
.msgw-badge-ok{background:rgba(16,185,129,0.15);color:var(--dsw-alias-state-success-primary, #10b981);border:1px solid rgba(16,185,129,0.3)}
|
|
264
|
+
.msgw-badge-warn{background:rgba(245,158,11,0.15);color:var(--dsw-alias-state-warning-primary, #f59e0b);border:1px solid rgba(245,158,11,0.3)}
|
|
265
|
+
.msgw-badge-bad{background:rgba(239,68,68,0.15);color:var(--dsw-alias-state-error-primary, #ef4444);border:1px solid rgba(239,68,68,0.3)}
|
|
266
|
+
|
|
267
|
+
.msgw-section-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:16px;display:flex;flex-direction:column;gap:12px}
|
|
268
|
+
.msgw-section-title{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between}
|
|
269
|
+
.msgw-section-desc{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.4;margin-top:-6px}
|
|
270
|
+
|
|
271
|
+
.msgw-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
|
272
|
+
.msgw-field{display:flex;flex-direction:column;gap:6px}
|
|
273
|
+
.msgw-label{font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary)}
|
|
274
|
+
.msgw-hint{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.3}
|
|
275
|
+
|
|
276
|
+
.msgw-input,.msgw-select,.msgw-textarea{width:100%;box-sizing:border-box;height:34px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font:inherit;font-size:13px;outline:none;transition:border-color .15s}
|
|
277
|
+
.msgw-input:focus,.msgw-select:focus,.msgw-textarea:focus{border-color:var(--dsw-alias-label-primary)}
|
|
278
|
+
.msgw-textarea{height:auto;min-height:72px;padding:8px 12px}
|
|
279
|
+
.msgw-check{display:flex;gap:8px;align-items:center;cursor:pointer;font-size:13px;color:var(--dsw-alias-label-primary);user-select:none}
|
|
280
|
+
|
|
281
|
+
.msgw-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:7px 14px;font-size:13px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-weight:500;display:inline-flex;align-items:center;justify-content:center;gap:6px;transition:all .15s ease}
|
|
282
|
+
.msgw-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-4, var(--dsw-alias-bg-layer-2));border-color:var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2))}
|
|
283
|
+
.msgw-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent}
|
|
284
|
+
.msgw-btn-primary:hover:not(:disabled){opacity:0.88}
|
|
285
|
+
.msgw-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:rgba(239,68,68,0.3)}
|
|
286
|
+
.msgw-btn-danger:hover:not(:disabled){background:rgba(239,68,68,0.12);border-color:rgba(239,68,68,0.5)}
|
|
287
|
+
.msgw-btn-mini{padding:4px 10px;font-size:12px;border-radius:6px}
|
|
288
|
+
.msgw-btn:disabled{opacity:0.5;cursor:not-allowed}
|
|
289
|
+
|
|
290
|
+
.msgw-alert-ok{padding:10px 14px;border-radius:8px;background:rgba(16,185,129,0.1);color:var(--dsw-alias-state-success-primary);font-size:13px;border:1px solid rgba(16,185,129,0.25)}
|
|
291
|
+
.msgw-alert-err{padding:10px 14px;border-radius:8px;background:rgba(239,68,68,0.1);color:var(--dsw-alias-state-error-primary);font-size:13px;border:1px solid rgba(239,68,68,0.25)}
|
|
292
|
+
.msgw-banner-warning{padding:12px 16px;border-radius:8px;background:rgba(245,158,11,0.12);border:1px solid var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);font-size:13px;display:flex;align-items:center;gap:10px;font-weight:500}
|
|
293
|
+
|
|
294
|
+
.msgw-stat-grid{display:grid;grid-template-columns:repeat(auto-fit, minmax(140px, 1fr));gap:10px}
|
|
295
|
+
.msgw-stat-box{padding:12px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:4px}
|
|
296
|
+
.msgw-stat-val{font-size:18px;font-weight:700;color:var(--dsw-alias-label-primary)}
|
|
297
|
+
.msgw-stat-lbl{font-size:12px;color:var(--dsw-alias-label-secondary)}
|
|
298
|
+
|
|
299
|
+
.msgw-table{width:100%;border-collapse:collapse;margin-top:4px}
|
|
300
|
+
.msgw-table th{text-align:left;font-size:12px;color:var(--dsw-alias-label-secondary);padding:6px 8px;border-bottom:1px solid var(--dsw-alias-border-l2);font-weight:600}
|
|
301
|
+
.msgw-table td{padding:8px;border-bottom:1px solid var(--dsw-alias-border-l2);font-size:13px;color:var(--dsw-alias-label-primary)}
|
|
302
|
+
.msgw-table tr:hover{background:var(--dsw-alias-bg-layer-2)}
|
|
303
|
+
.msgw-preview{padding:12px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);font-family:monospace;font-size:12px;white-space:pre-wrap;word-break:break-all;border:1px solid var(--dsw-alias-border-l2)}
|
|
304
|
+
`
|
|
305
|
+
document.head.appendChild(style)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function createErrorBoundary() {
|
|
309
|
+
if (!React || typeof React.Component !== 'function') {
|
|
310
|
+
return function NoopBoundary(props) { return props?.children || null }
|
|
311
|
+
}
|
|
312
|
+
return class ErrorBoundary extends React.Component {
|
|
313
|
+
constructor(props) {
|
|
314
|
+
super(props)
|
|
315
|
+
this.state = { hasError: false, error: null }
|
|
316
|
+
}
|
|
317
|
+
static getDerivedStateFromError(error) {
|
|
318
|
+
return { hasError: true, error }
|
|
319
|
+
}
|
|
320
|
+
componentDidCatch(error, errorInfo) {
|
|
321
|
+
console.error('[dsh-messenger-gateway] React UI Error:', error, errorInfo)
|
|
322
|
+
}
|
|
323
|
+
render() {
|
|
324
|
+
if (this.state.hasError) {
|
|
325
|
+
return React.createElement(
|
|
326
|
+
'div',
|
|
327
|
+
{ className: 'msgw-alert-err', style: { margin: '12px 0', padding: '14px', borderRadius: '8px' } },
|
|
328
|
+
React.createElement('div', { style: { fontWeight: 600, marginBottom: '6px' } }, '⚠️ Messenger Gateway UI Error:'),
|
|
329
|
+
React.createElement('div', { style: { fontSize: '12px', wordBreak: 'break-all' } }, String(this.state.error?.message || this.state.error)),
|
|
330
|
+
React.createElement(
|
|
331
|
+
'button',
|
|
332
|
+
{
|
|
333
|
+
type: 'button',
|
|
334
|
+
className: 'msgw-btn msgw-btn-mini',
|
|
335
|
+
style: { marginTop: '10px' },
|
|
336
|
+
onClick: () => this.setState({ hasError: false, error: null }),
|
|
337
|
+
},
|
|
338
|
+
'Retry'
|
|
339
|
+
)
|
|
340
|
+
)
|
|
341
|
+
}
|
|
342
|
+
return this.props?.children || null
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const ErrorBoundary = createErrorBoundary()
|
|
347
|
+
|
|
348
|
+
function FallbackChevron() {
|
|
349
|
+
return React.createElement(
|
|
350
|
+
'svg',
|
|
351
|
+
{ width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' },
|
|
352
|
+
React.createElement('polyline', { points: '6 9 12 15 18 9' })
|
|
353
|
+
)
|
|
354
|
+
}
|
|
355
|
+
const Chevron = FallbackChevron
|
|
356
|
+
|
|
357
|
+
function idsToText(ids) { return (Array.isArray(ids) ? ids : []).map(String).filter(Boolean).join(', ') }
|
|
358
|
+
function textToIds(text) {
|
|
359
|
+
return String(text || '').split(/[\s,]+/).map((s) => s.trim()).filter(Boolean).map(Number).filter((n) => Number.isFinite(n))
|
|
360
|
+
}
|
|
361
|
+
function bytesToMb(n) { return Math.round(Number(n || 0) / (1024 * 1024)) }
|
|
362
|
+
function mbToBytes(n) { return Math.max(1, Math.round(Number(n || 1))) * 1024 * 1024 }
|
|
363
|
+
|
|
364
|
+
function Field({ label, hint, children }) {
|
|
365
|
+
return React.createElement('div', { className: 'msgw-field' },
|
|
366
|
+
React.createElement('label', { className: 'msgw-label' }, label),
|
|
367
|
+
hint ? React.createElement('div', { className: 'msgw-hint' }, hint) : null,
|
|
368
|
+
children,
|
|
369
|
+
)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const SETTINGS_KEYS = ['enabled', 'telegram', 'agent', 'media', 'tts']
|
|
373
|
+
|
|
374
|
+
function draftFromStored(s) {
|
|
375
|
+
const src = (s && typeof s === 'object') ? s : {}
|
|
376
|
+
const t = src.telegram || {}
|
|
377
|
+
const a = src.agent || {}
|
|
378
|
+
const m = src.media || {}
|
|
379
|
+
const tts = src.tts || {}
|
|
380
|
+
return {
|
|
381
|
+
enabled: src.enabled !== false,
|
|
382
|
+
telegram: {
|
|
383
|
+
enabled: t.enabled === true,
|
|
384
|
+
allowedUserIds: Array.isArray(t.allowedUserIds) ? t.allowedUserIds : [],
|
|
385
|
+
pollTimeoutSeconds: Number(t.pollTimeoutSeconds) || 50,
|
|
386
|
+
pollIntervalMs: Number(t.pollIntervalMs) || 500,
|
|
387
|
+
commands: Array.isArray(t.commands) ? t.commands : [],
|
|
388
|
+
textFormat: t.textFormat === 'plain' ? 'plain' : 'html',
|
|
389
|
+
homeChatId: t.homeChatId != null ? t.homeChatId : '',
|
|
390
|
+
homeThreadId: Number(t.homeThreadId) || 0,
|
|
391
|
+
homes: Array.isArray(t.homes) ? t.homes : [],
|
|
392
|
+
pairingEnabled: t.pairingEnabled !== false,
|
|
393
|
+
streaming: t.streaming === true,
|
|
394
|
+
streamEditIntervalMs: Number(t.streamEditIntervalMs) || 1200,
|
|
395
|
+
progressEnabled: t.progressEnabled !== false,
|
|
396
|
+
approvalsEnabled: t.approvalsEnabled !== false,
|
|
397
|
+
groupsEnabled: t.groupsEnabled !== false,
|
|
398
|
+
groupRequireMention: t.groupRequireMention !== false,
|
|
399
|
+
reactionsEnabled: t.reactionsEnabled !== false,
|
|
400
|
+
statusIndicator: t.statusIndicator === true,
|
|
401
|
+
statusOnline: t.statusOnline || 'Online',
|
|
402
|
+
statusOffline: t.statusOffline || 'Offline',
|
|
403
|
+
transport: t.transport === 'webhook' ? 'webhook' : 'poll',
|
|
404
|
+
webhookUrl: t.webhookUrl || '',
|
|
405
|
+
webhookPath: t.webhookPath || '/dsh-messenger-gateway/telegram/webhook',
|
|
406
|
+
voiceMode: t.voiceMode || 'mirror',
|
|
407
|
+
quickActions: t.quickActions === true,
|
|
408
|
+
artifactPreviews: t.artifactPreviews !== false,
|
|
409
|
+
notifyBridge: t.notifyBridge || { enabled: false, events: ['task_done', 'error'], home: 'default', excludeSessionPrefixes: ['msgw-'] },
|
|
410
|
+
alerts: t.alerts || { enabled: false, chatId: '', threadId: 0, home: '', events: ['error', 'pairing'] },
|
|
411
|
+
botToken: t.botToken || '',
|
|
412
|
+
webhookSecret: t.webhookSecret || '',
|
|
413
|
+
},
|
|
414
|
+
agent: {
|
|
415
|
+
provider: a.provider || '',
|
|
416
|
+
model: a.model || '',
|
|
417
|
+
instructionPrefix: a.instructionPrefix || '',
|
|
418
|
+
maxMessageLength: Number(a.maxMessageLength) || 4000,
|
|
419
|
+
turnTimeoutMs: Number(a.turnTimeoutMs) || 600000,
|
|
420
|
+
idleTimeoutMs: Number(a.idleTimeoutMs) || 3600000,
|
|
421
|
+
photoOnlyMode: a.photoOnlyMode || 'prompt',
|
|
422
|
+
sessionScope: a.sessionScope || 'user',
|
|
423
|
+
},
|
|
424
|
+
media: {
|
|
425
|
+
maxDocBytes: Number(m.maxDocBytes) || 20971520,
|
|
426
|
+
maxImageBytes: Number(m.maxImageBytes) || 20971520,
|
|
427
|
+
maxTextInjectBytes: Number(m.maxTextInjectBytes) || 102400,
|
|
428
|
+
},
|
|
429
|
+
tts: {
|
|
430
|
+
enabled: tts.enabled === true,
|
|
431
|
+
maxChars: Number(tts.maxChars) || 4000,
|
|
432
|
+
voiceSummary: tts.voiceSummary === true,
|
|
433
|
+
},
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function SettingsPage(props) {
|
|
438
|
+
const ctx = props?.ctx
|
|
439
|
+
const t = props?.t || makeT(props?.locale)
|
|
440
|
+
|
|
441
|
+
const [token, setToken] = React.useState('')
|
|
442
|
+
const [webhookSecret, setWebhookSecret] = React.useState('')
|
|
443
|
+
const [allowText, setAllowText] = React.useState('')
|
|
444
|
+
const [pending, setPending] = React.useState([])
|
|
445
|
+
const [status, setStatus] = React.useState(null)
|
|
446
|
+
const [smokeResult, setSmokeResult] = React.useState(null)
|
|
447
|
+
const [err, setErr] = React.useState('')
|
|
448
|
+
const [msg, setMsg] = React.useState('')
|
|
449
|
+
const [busy, setBusy] = React.useState('')
|
|
450
|
+
const [showAdvanced, setShowAdvanced] = React.useState(false)
|
|
451
|
+
|
|
452
|
+
React.useEffect(() => {
|
|
453
|
+
ensureCss()
|
|
454
|
+
}, [])
|
|
455
|
+
|
|
456
|
+
const scope = React.useMemo(() => {
|
|
457
|
+
const settingsScope = ctx?.get?.('settingsScope') || ctx?.settingsScope
|
|
458
|
+
return settingsScope ? settingsScope.bind({ namespace: NS }) : undefined
|
|
459
|
+
}, [ctx])
|
|
460
|
+
|
|
461
|
+
const snapshot = React.useSyncExternalStore(
|
|
462
|
+
React.useMemo(() => (cb) => (scope ? scope.subscribe(cb) : () => {}), [scope]),
|
|
463
|
+
React.useCallback(() => (scope ? scope.getSnapshot() : { status: 'loading' }), [scope]),
|
|
464
|
+
React.useCallback(() => ({ status: 'loading' }), []),
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
const snapStatus = (snapshot && snapshot.status) || 'loading'
|
|
468
|
+
const stored = (snapshot && snapshot.value) || {}
|
|
469
|
+
const [cfg, setCfg] = React.useState(null)
|
|
470
|
+
|
|
471
|
+
React.useEffect(() => {
|
|
472
|
+
if (snapStatus === 'ready' && cfg === null) {
|
|
473
|
+
setCfg(draftFromStored(stored))
|
|
474
|
+
setAllowText(idsToText(stored?.telegram?.allowedUserIds))
|
|
475
|
+
}
|
|
476
|
+
}, [snapStatus, stored, cfg])
|
|
477
|
+
|
|
478
|
+
const loadStatus = React.useCallback(async () => {
|
|
479
|
+
try {
|
|
480
|
+
const res = await fetch(`${ROUTE_PREFIX}/status`, { cache: 'no-store' })
|
|
481
|
+
const data = await res.json().catch(() => ({}))
|
|
482
|
+
if (res.ok && data.ok) {
|
|
483
|
+
setStatus(data)
|
|
484
|
+
}
|
|
485
|
+
} catch (_) {}
|
|
486
|
+
}, [])
|
|
487
|
+
|
|
488
|
+
const loadPairing = React.useCallback(async () => {
|
|
489
|
+
try {
|
|
490
|
+
const res = await fetch(`${ROUTE_PREFIX}/pairing`, { credentials: 'same-origin', cache: 'no-store' })
|
|
491
|
+
const data = await res.json().catch(() => ({}))
|
|
492
|
+
if (res.ok && data.ok) setPending(Array.isArray(data.pending) ? data.pending : [])
|
|
493
|
+
} catch (_) {}
|
|
494
|
+
}, [])
|
|
495
|
+
|
|
496
|
+
React.useEffect(() => {
|
|
497
|
+
loadStatus()
|
|
498
|
+
loadPairing()
|
|
499
|
+
}, [loadStatus, loadPairing])
|
|
500
|
+
|
|
501
|
+
const handleSmoke = async () => {
|
|
502
|
+
setBusy('smoke')
|
|
503
|
+
setErr('')
|
|
504
|
+
setMsg('')
|
|
505
|
+
setSmokeResult(null)
|
|
506
|
+
try {
|
|
507
|
+
const res = await fetch(`${ROUTE_PREFIX}/smoke`, {
|
|
508
|
+
method: 'POST',
|
|
509
|
+
credentials: 'same-origin',
|
|
510
|
+
headers: { 'Content-Type': 'application/json' },
|
|
511
|
+
body: JSON.stringify({ timeoutMs: 8000 }),
|
|
512
|
+
})
|
|
513
|
+
const data = await res.json().catch(() => ({}))
|
|
514
|
+
if (!data.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
515
|
+
setSmokeResult(data)
|
|
516
|
+
setMsg(t('diag.smoke_ok', { latency: data.latencyMs, username: data.botUsername, id: data.botId }))
|
|
517
|
+
await loadStatus()
|
|
518
|
+
} catch (e) {
|
|
519
|
+
setErr(t('diag.smoke_err', { error: String(e.message || e) }))
|
|
520
|
+
} finally {
|
|
521
|
+
setBusy('')
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const mergePatch = (base, patch) => ({
|
|
526
|
+
...base,
|
|
527
|
+
...patch,
|
|
528
|
+
telegram: { ...base.telegram, ...(patch.telegram || {}) },
|
|
529
|
+
tts: { ...base.tts, ...(patch.tts || {}) },
|
|
530
|
+
agent: { ...base.agent, ...(patch.agent || {}) },
|
|
531
|
+
media: { ...base.media, ...(patch.media || {}) },
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
const save = async (patch = {}) => {
|
|
535
|
+
if (!scope || !cfg) return
|
|
536
|
+
setBusy('save')
|
|
537
|
+
setErr('')
|
|
538
|
+
setMsg('')
|
|
539
|
+
try {
|
|
540
|
+
let next = mergePatch(cfg, patch)
|
|
541
|
+
if (token.trim()) next.telegram = { ...next.telegram, botToken: token.trim() }
|
|
542
|
+
if (webhookSecret.trim()) next.telegram = { ...next.telegram, webhookSecret: webhookSecret.trim() }
|
|
543
|
+
next.telegram.allowedUserIds = textToIds(allowText)
|
|
544
|
+
|
|
545
|
+
const broken = []
|
|
546
|
+
for (const key of SETTINGS_KEYS) {
|
|
547
|
+
if (next[key] !== undefined) {
|
|
548
|
+
try {
|
|
549
|
+
await scope.set(key, next[key])
|
|
550
|
+
} catch (e) {
|
|
551
|
+
broken.push(key + ': ' + (e && e.message || String(e)))
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
if (broken.length) {
|
|
556
|
+
setErr('Save failed — ' + broken.join('; '))
|
|
557
|
+
return
|
|
558
|
+
}
|
|
559
|
+
setCfg(next)
|
|
560
|
+
setToken('')
|
|
561
|
+
setWebhookSecret('')
|
|
562
|
+
setMsg(t('saved'))
|
|
563
|
+
setTimeout(() => setMsg(''), 3000)
|
|
564
|
+
await loadPairing()
|
|
565
|
+
await loadStatus()
|
|
566
|
+
} catch (e) {
|
|
567
|
+
setErr(String(e.message || e))
|
|
568
|
+
} finally {
|
|
569
|
+
setBusy('')
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (snapStatus === 'loading') {
|
|
574
|
+
return React.createElement('div', { className: 'msgw-page' }, t('settings.loading'))
|
|
575
|
+
}
|
|
576
|
+
if (snapStatus !== 'ready') {
|
|
577
|
+
return React.createElement('div', { className: 'msgw-page' },
|
|
578
|
+
React.createElement('div', { className: 'msgw-banner-warning' }, t('settings.unavailable'))
|
|
579
|
+
)
|
|
580
|
+
}
|
|
581
|
+
if (!cfg) {
|
|
582
|
+
return React.createElement('div', { className: 'msgw-page' }, t('settings.loading'))
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const isRunning = Boolean(status?.running)
|
|
586
|
+
const botUsername = status?.botUsername || (status?.config?.telegram?.botUsername) || ''
|
|
587
|
+
const hasToken = Boolean(status?.config?.telegram?.botTokenConfigured || cfg.telegram?.botTokenConfigured || token.trim())
|
|
588
|
+
const transportMode = cfg.telegram?.transport || 'poll'
|
|
589
|
+
const pendingCount = pending.length
|
|
590
|
+
|
|
591
|
+
return React.createElement(
|
|
592
|
+
'div',
|
|
593
|
+
{ className: 'msgw-page' },
|
|
594
|
+
|
|
595
|
+
// Header with badges
|
|
596
|
+
React.createElement(
|
|
597
|
+
'div',
|
|
598
|
+
{ className: 'msgw-header' },
|
|
599
|
+
React.createElement(
|
|
600
|
+
'div',
|
|
601
|
+
{ className: 'msgw-page-title' },
|
|
602
|
+
`💬 ${t('header.title')}`,
|
|
603
|
+
React.createElement(
|
|
604
|
+
'span',
|
|
605
|
+
{ className: `msgw-badge ${isRunning ? 'msgw-badge-ok' : 'msgw-badge-bad'}` },
|
|
606
|
+
isRunning
|
|
607
|
+
? (botUsername ? t('badge.online', { username: botUsername }) : t('badge.running'))
|
|
608
|
+
: t('badge.stopped')
|
|
609
|
+
),
|
|
610
|
+
React.createElement(
|
|
611
|
+
'span',
|
|
612
|
+
{ className: `msgw-badge ${hasToken ? 'msgw-badge-ok' : 'msgw-badge-warn'}` },
|
|
613
|
+
hasToken ? t('badge.token_ok') : t('badge.token_missing')
|
|
614
|
+
),
|
|
615
|
+
React.createElement(
|
|
616
|
+
'span',
|
|
617
|
+
{ className: 'msgw-badge msgw-badge-ok' },
|
|
618
|
+
t('badge.transport', { mode: transportMode })
|
|
619
|
+
),
|
|
620
|
+
pendingCount > 0
|
|
621
|
+
? React.createElement(
|
|
622
|
+
'span',
|
|
623
|
+
{ className: 'msgw-badge msgw-badge-warn' },
|
|
624
|
+
t('badge.pairing_count', { count: pendingCount })
|
|
625
|
+
)
|
|
626
|
+
: null
|
|
627
|
+
),
|
|
628
|
+
React.createElement('div', { className: 'msgw-page-sub' }, t('header.sub'))
|
|
629
|
+
),
|
|
630
|
+
|
|
631
|
+
// Global Alert banners
|
|
632
|
+
err ? React.createElement('div', { className: 'msgw-alert-err' }, err) : null,
|
|
633
|
+
msg ? React.createElement('div', { className: 'msgw-alert-ok' }, msg) : null,
|
|
634
|
+
|
|
635
|
+
// Card 1: Telegram Connection & Bot
|
|
636
|
+
React.createElement(
|
|
637
|
+
'div',
|
|
638
|
+
{ className: 'msgw-section-card' },
|
|
639
|
+
React.createElement(
|
|
640
|
+
'div',
|
|
641
|
+
{ className: 'msgw-section-title' },
|
|
642
|
+
React.createElement('span', null, `🤖 ${t('telegram')}`),
|
|
643
|
+
React.createElement(
|
|
644
|
+
'button',
|
|
645
|
+
{
|
|
646
|
+
type: 'button',
|
|
647
|
+
className: `msgw-btn msgw-btn-mini ${cfg.telegram.enabled ? 'msgw-btn-danger' : 'msgw-btn-primary'}`,
|
|
648
|
+
disabled: !!busy,
|
|
649
|
+
onClick: () => save({ telegram: { enabled: !cfg.telegram.enabled } }),
|
|
650
|
+
},
|
|
651
|
+
cfg.telegram.enabled ? t('disableTelegram') : t('enableTelegram')
|
|
652
|
+
)
|
|
653
|
+
),
|
|
654
|
+
React.createElement('div', { className: 'msgw-section-desc' }, t('telegramSub')),
|
|
655
|
+
React.createElement(Field, { label: t('botToken'), hint: t('botTokenHint') },
|
|
656
|
+
React.createElement('input', {
|
|
657
|
+
type: 'password',
|
|
658
|
+
className: 'msgw-input',
|
|
659
|
+
value: token,
|
|
660
|
+
placeholder: hasToken ? '••••••••••••••••••••' : '123456789:ABCdefGHI...',
|
|
661
|
+
onChange: (e) => setToken(e.target.value),
|
|
662
|
+
})
|
|
663
|
+
),
|
|
664
|
+
React.createElement(Field, { label: t('allowedIds'), hint: t('allowedIdsHint') },
|
|
665
|
+
React.createElement('textarea', {
|
|
666
|
+
className: 'msgw-textarea',
|
|
667
|
+
value: allowText,
|
|
668
|
+
onChange: (e) => setAllowText(e.target.value),
|
|
669
|
+
rows: 2,
|
|
670
|
+
placeholder: '108191529, 987654321',
|
|
671
|
+
})
|
|
672
|
+
),
|
|
673
|
+
React.createElement(Field, { label: t('textFormat') },
|
|
674
|
+
React.createElement(
|
|
675
|
+
'select',
|
|
676
|
+
{
|
|
677
|
+
className: 'msgw-select',
|
|
678
|
+
value: cfg.telegram?.textFormat || 'html',
|
|
679
|
+
onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, textFormat: e.target.value } }),
|
|
680
|
+
},
|
|
681
|
+
React.createElement('option', { value: 'html' }, t('textFormatHtml')),
|
|
682
|
+
React.createElement('option', { value: 'plain' }, t('textFormatPlain'))
|
|
683
|
+
)
|
|
684
|
+
),
|
|
685
|
+
React.createElement('label', { className: 'msgw-check' },
|
|
686
|
+
React.createElement('input', {
|
|
687
|
+
type: 'checkbox',
|
|
688
|
+
checked: cfg.telegram?.groupsEnabled !== false,
|
|
689
|
+
onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, groupsEnabled: e.target.checked } }),
|
|
690
|
+
}),
|
|
691
|
+
t('groupsEnable')
|
|
692
|
+
),
|
|
693
|
+
React.createElement('label', { className: 'msgw-check' },
|
|
694
|
+
React.createElement('input', {
|
|
695
|
+
type: 'checkbox',
|
|
696
|
+
checked: cfg.telegram?.groupRequireMention !== false,
|
|
697
|
+
onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, groupRequireMention: e.target.checked } }),
|
|
698
|
+
}),
|
|
699
|
+
t('groupMention')
|
|
700
|
+
),
|
|
701
|
+
React.createElement('label', { className: 'msgw-check' },
|
|
702
|
+
React.createElement('input', {
|
|
703
|
+
type: 'checkbox',
|
|
704
|
+
checked: cfg.telegram?.reactionsEnabled !== false,
|
|
705
|
+
onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, reactionsEnabled: e.target.checked } }),
|
|
706
|
+
}),
|
|
707
|
+
t('reactionsEnable')
|
|
708
|
+
),
|
|
709
|
+
React.createElement('label', { className: 'msgw-check' },
|
|
710
|
+
React.createElement('input', {
|
|
711
|
+
type: 'checkbox',
|
|
712
|
+
checked: cfg.telegram?.progressEnabled !== false,
|
|
713
|
+
onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, progressEnabled: e.target.checked } }),
|
|
714
|
+
}),
|
|
715
|
+
t('progressEnable')
|
|
716
|
+
),
|
|
717
|
+
React.createElement('label', { className: 'msgw-check' },
|
|
718
|
+
React.createElement('input', {
|
|
719
|
+
type: 'checkbox',
|
|
720
|
+
checked: cfg.telegram?.quickActions !== false,
|
|
721
|
+
onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, quickActions: e.target.checked } }),
|
|
722
|
+
}),
|
|
723
|
+
t('quickActions')
|
|
724
|
+
),
|
|
725
|
+
React.createElement('label', { className: 'msgw-check' },
|
|
726
|
+
React.createElement('input', {
|
|
727
|
+
type: 'checkbox',
|
|
728
|
+
checked: cfg.telegram?.artifactPreviews !== false,
|
|
729
|
+
onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, artifactPreviews: e.target.checked } }),
|
|
730
|
+
}),
|
|
731
|
+
t('artifactPreviews')
|
|
732
|
+
)
|
|
733
|
+
),
|
|
734
|
+
|
|
735
|
+
// Card 2: Diagnostics & Telegram Smoke Test
|
|
736
|
+
React.createElement(
|
|
737
|
+
'div',
|
|
738
|
+
{ className: 'msgw-section-card' },
|
|
739
|
+
React.createElement('div', { className: 'msgw-section-title' }, t('diag.title')),
|
|
740
|
+
React.createElement('div', { className: 'msgw-section-desc' }, t('diag.desc')),
|
|
741
|
+
React.createElement(
|
|
742
|
+
'div',
|
|
743
|
+
{ className: 'msgw-row' },
|
|
744
|
+
React.createElement(
|
|
745
|
+
'button',
|
|
746
|
+
{
|
|
747
|
+
type: 'button',
|
|
748
|
+
className: 'msgw-btn msgw-btn-primary',
|
|
749
|
+
disabled: !!busy || !hasToken,
|
|
750
|
+
onClick: handleSmoke,
|
|
751
|
+
},
|
|
752
|
+
busy === 'smoke' ? t('diag.smoke_testing') : t('diag.smoke_btn')
|
|
753
|
+
)
|
|
754
|
+
),
|
|
755
|
+
smokeResult
|
|
756
|
+
? React.createElement(
|
|
757
|
+
'div',
|
|
758
|
+
{ className: 'msgw-preview' },
|
|
759
|
+
`✅ Latency: ${smokeResult.latencyMs} ms | Bot: @${smokeResult.botUsername || 'unknown'} (ID: ${smokeResult.botId})`
|
|
760
|
+
)
|
|
761
|
+
: null
|
|
762
|
+
),
|
|
763
|
+
|
|
764
|
+
// Card 3: Telemetry & Gateway Counters
|
|
765
|
+
status?.stats ? React.createElement(
|
|
766
|
+
'div',
|
|
767
|
+
{ className: 'msgw-section-card' },
|
|
768
|
+
React.createElement('div', { className: 'msgw-section-title' }, t('stats.title')),
|
|
769
|
+
React.createElement('div', { className: 'msgw-section-desc' }, t('stats.desc')),
|
|
770
|
+
React.createElement(
|
|
771
|
+
'div',
|
|
772
|
+
{ className: 'msgw-stat-grid' },
|
|
773
|
+
React.createElement(
|
|
774
|
+
'div',
|
|
775
|
+
{ className: 'msgw-stat-box' },
|
|
776
|
+
React.createElement('div', { className: 'msgw-stat-val' }, String(status.stats.sent || 0)),
|
|
777
|
+
React.createElement('div', { className: 'msgw-stat-lbl' }, t('stats.sent'))
|
|
778
|
+
),
|
|
779
|
+
React.createElement(
|
|
780
|
+
'div',
|
|
781
|
+
{ className: 'msgw-stat-box' },
|
|
782
|
+
React.createElement('div', { className: 'msgw-stat-val' }, String(status.stats.errors || 0)),
|
|
783
|
+
React.createElement('div', { className: 'msgw-stat-lbl' }, t('stats.errors'))
|
|
784
|
+
),
|
|
785
|
+
React.createElement(
|
|
786
|
+
'div',
|
|
787
|
+
{ className: 'msgw-stat-box' },
|
|
788
|
+
React.createElement('div', { className: 'msgw-stat-val' }, String(status.activeChats || 0)),
|
|
789
|
+
React.createElement('div', { className: 'msgw-stat-lbl' }, t('stats.active_chats'))
|
|
790
|
+
),
|
|
791
|
+
React.createElement(
|
|
792
|
+
'div',
|
|
793
|
+
{ className: 'msgw-stat-box' },
|
|
794
|
+
React.createElement('div', { className: 'msgw-stat-val' }, `${status.uptimeSec || 0}s`),
|
|
795
|
+
React.createElement('div', { className: 'msgw-stat-lbl' }, t('stats.uptime'))
|
|
796
|
+
)
|
|
797
|
+
)
|
|
798
|
+
) : null,
|
|
799
|
+
|
|
800
|
+
// Card 4: Agent & Turn Execution
|
|
801
|
+
React.createElement(
|
|
802
|
+
'div',
|
|
803
|
+
{ className: 'msgw-section-card' },
|
|
804
|
+
React.createElement('div', { className: 'msgw-section-title' }, `🧠 ${t('agent')}`),
|
|
805
|
+
React.createElement('div', { className: 'msgw-section-desc' }, t('agentSub')),
|
|
806
|
+
React.createElement(Field, { label: t('provider'), hint: t('providerHint') },
|
|
807
|
+
React.createElement('input', {
|
|
808
|
+
className: 'msgw-input',
|
|
809
|
+
value: cfg.agent?.provider || '',
|
|
810
|
+
onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, provider: e.target.value } }),
|
|
811
|
+
})
|
|
812
|
+
),
|
|
813
|
+
React.createElement(Field, { label: t('model'), hint: t('modelHint') },
|
|
814
|
+
React.createElement('input', {
|
|
815
|
+
className: 'msgw-input',
|
|
816
|
+
value: cfg.agent?.model || '',
|
|
817
|
+
onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, model: e.target.value } }),
|
|
818
|
+
})
|
|
819
|
+
),
|
|
820
|
+
React.createElement(Field, { label: t('photoOnly') },
|
|
821
|
+
React.createElement(
|
|
822
|
+
'select',
|
|
823
|
+
{
|
|
824
|
+
className: 'msgw-select',
|
|
825
|
+
value: cfg.agent?.photoOnlyMode || 'prompt',
|
|
826
|
+
onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, photoOnlyMode: e.target.value } }),
|
|
827
|
+
},
|
|
828
|
+
React.createElement('option', { value: 'prompt' }, t('photoPrompt')),
|
|
829
|
+
React.createElement('option', { value: 'run' }, t('photoRun'))
|
|
830
|
+
)
|
|
831
|
+
),
|
|
832
|
+
React.createElement(Field, { label: t('sessionScope') },
|
|
833
|
+
React.createElement(
|
|
834
|
+
'select',
|
|
835
|
+
{
|
|
836
|
+
className: 'msgw-select',
|
|
837
|
+
value: cfg.agent?.sessionScope || 'user',
|
|
838
|
+
onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, sessionScope: e.target.value } }),
|
|
839
|
+
},
|
|
840
|
+
React.createElement('option', { value: 'user' }, t('sessionScopeUser')),
|
|
841
|
+
React.createElement('option', { value: 'chat' }, t('sessionScopeChat'))
|
|
842
|
+
)
|
|
843
|
+
),
|
|
844
|
+
React.createElement(Field, { label: t('instruction'), hint: t('instructionHint') },
|
|
845
|
+
React.createElement('textarea', {
|
|
846
|
+
className: 'msgw-textarea',
|
|
847
|
+
value: cfg.agent?.instructionPrefix || '',
|
|
848
|
+
onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, instructionPrefix: e.target.value } }),
|
|
849
|
+
rows: 3,
|
|
850
|
+
placeholder: t('instructionPlaceholder'),
|
|
851
|
+
})
|
|
852
|
+
),
|
|
853
|
+
React.createElement(Field, { label: t('maxMessageLength') },
|
|
854
|
+
React.createElement('input', {
|
|
855
|
+
type: 'number',
|
|
856
|
+
className: 'msgw-input',
|
|
857
|
+
min: 500,
|
|
858
|
+
max: 8000,
|
|
859
|
+
value: cfg.agent?.maxMessageLength ?? 4000,
|
|
860
|
+
onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, maxMessageLength: Number(e.target.value) || 4000 } }),
|
|
861
|
+
})
|
|
862
|
+
),
|
|
863
|
+
React.createElement(Field, { label: t('turnTimeout') },
|
|
864
|
+
React.createElement('input', {
|
|
865
|
+
type: 'number',
|
|
866
|
+
className: 'msgw-input',
|
|
867
|
+
min: 30,
|
|
868
|
+
max: 3600,
|
|
869
|
+
value: Math.round((cfg.agent?.turnTimeoutMs ?? 600000) / 1000),
|
|
870
|
+
onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, turnTimeoutMs: (Number(e.target.value) || 600) * 1000 } }),
|
|
871
|
+
})
|
|
872
|
+
)
|
|
873
|
+
),
|
|
874
|
+
|
|
875
|
+
// Card 5: Media & TTS
|
|
876
|
+
React.createElement(
|
|
877
|
+
'div',
|
|
878
|
+
{ className: 'msgw-section-card' },
|
|
879
|
+
React.createElement('div', { className: 'msgw-section-title' }, `📎 ${t('media')}`),
|
|
880
|
+
React.createElement('div', { className: 'msgw-section-desc' }, t('mediaSub')),
|
|
881
|
+
React.createElement(Field, { label: t('maxDocMb') },
|
|
882
|
+
React.createElement('input', {
|
|
883
|
+
type: 'number',
|
|
884
|
+
className: 'msgw-input',
|
|
885
|
+
min: 1,
|
|
886
|
+
max: 50,
|
|
887
|
+
value: bytesToMb(cfg.media?.maxDocBytes),
|
|
888
|
+
onChange: (e) => setCfg({ ...cfg, media: { ...cfg.media, maxDocBytes: mbToBytes(e.target.value) } }),
|
|
889
|
+
})
|
|
890
|
+
),
|
|
891
|
+
React.createElement(Field, { label: t('maxImageMb') },
|
|
892
|
+
React.createElement('input', {
|
|
893
|
+
type: 'number',
|
|
894
|
+
className: 'msgw-input',
|
|
895
|
+
min: 1,
|
|
896
|
+
max: 50,
|
|
897
|
+
value: bytesToMb(cfg.media?.maxImageBytes),
|
|
898
|
+
onChange: (e) => setCfg({ ...cfg, media: { ...cfg.media, maxImageBytes: mbToBytes(e.target.value) } }),
|
|
899
|
+
})
|
|
900
|
+
),
|
|
901
|
+
React.createElement(Field, { label: t('maxTextKb') },
|
|
902
|
+
React.createElement('input', {
|
|
903
|
+
type: 'number',
|
|
904
|
+
className: 'msgw-input',
|
|
905
|
+
min: 16,
|
|
906
|
+
max: 512,
|
|
907
|
+
value: Math.round((cfg.media?.maxTextInjectBytes ?? 102400) / 1024),
|
|
908
|
+
onChange: (e) => setCfg({ ...cfg, media: { ...cfg.media, maxTextInjectBytes: (Number(e.target.value) || 100) * 1024 } }),
|
|
909
|
+
})
|
|
910
|
+
),
|
|
911
|
+
React.createElement('div', { style: { height: '8px' } }),
|
|
912
|
+
React.createElement('div', { className: 'msgw-section-title' }, `🔊 ${t('tts')}`),
|
|
913
|
+
React.createElement('div', { className: 'msgw-section-desc' }, t('ttsSub')),
|
|
914
|
+
React.createElement('label', { className: 'msgw-check' },
|
|
915
|
+
React.createElement('input', {
|
|
916
|
+
type: 'checkbox',
|
|
917
|
+
checked: !!cfg.tts?.enabled,
|
|
918
|
+
onChange: (e) => setCfg({ ...cfg, tts: { ...cfg.tts, enabled: e.target.checked } }),
|
|
919
|
+
}),
|
|
920
|
+
t('ttsEnable')
|
|
921
|
+
),
|
|
922
|
+
cfg.tts?.enabled ? React.createElement(Field, { label: t('ttsMaxChars') },
|
|
923
|
+
React.createElement('input', {
|
|
924
|
+
type: 'number',
|
|
925
|
+
className: 'msgw-input',
|
|
926
|
+
min: 100,
|
|
927
|
+
max: 8000,
|
|
928
|
+
value: cfg.tts?.maxChars ?? 4000,
|
|
929
|
+
onChange: (e) => setCfg({ ...cfg, tts: { ...cfg.tts, maxChars: Number(e.target.value) || 4000 } }),
|
|
930
|
+
})
|
|
931
|
+
) : null,
|
|
932
|
+
React.createElement(Field, { label: t('voiceMode') },
|
|
933
|
+
React.createElement(
|
|
934
|
+
'select',
|
|
935
|
+
{
|
|
936
|
+
className: 'msgw-select',
|
|
937
|
+
value: cfg.telegram?.voiceMode || 'mirror',
|
|
938
|
+
onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, voiceMode: e.target.value } }),
|
|
939
|
+
},
|
|
940
|
+
React.createElement('option', { value: 'mirror' }, t('voiceModeMirror')),
|
|
941
|
+
React.createElement('option', { value: 'always' }, t('voiceModeAlways')),
|
|
942
|
+
React.createElement('option', { value: 'off' }, t('voiceModeOff'))
|
|
943
|
+
)
|
|
944
|
+
)
|
|
945
|
+
),
|
|
946
|
+
|
|
947
|
+
// Card 6: Pairing Requests
|
|
948
|
+
React.createElement(
|
|
949
|
+
'div',
|
|
950
|
+
{ className: 'msgw-section-card' },
|
|
951
|
+
React.createElement(
|
|
952
|
+
'div',
|
|
953
|
+
{ className: 'msgw-section-title' },
|
|
954
|
+
React.createElement('span', null, `🔑 ${t('pairing')}`),
|
|
955
|
+
React.createElement(
|
|
956
|
+
'button',
|
|
957
|
+
{
|
|
958
|
+
type: 'button',
|
|
959
|
+
className: 'msgw-btn msgw-btn-mini',
|
|
960
|
+
disabled: !!busy,
|
|
961
|
+
onClick: loadPairing,
|
|
962
|
+
},
|
|
963
|
+
t('pairingRefresh')
|
|
964
|
+
)
|
|
965
|
+
),
|
|
966
|
+
React.createElement('div', { className: 'msgw-section-desc' }, t('pairingSub')),
|
|
967
|
+
!(pending && pending.length)
|
|
968
|
+
? React.createElement('div', { className: 'msgw-hint' }, t('pairingEmpty'))
|
|
969
|
+
: React.createElement(
|
|
970
|
+
'table',
|
|
971
|
+
{ className: 'msgw-table' },
|
|
972
|
+
React.createElement(
|
|
973
|
+
'tbody',
|
|
974
|
+
null,
|
|
975
|
+
pending.map((row) =>
|
|
976
|
+
React.createElement(
|
|
977
|
+
'tr',
|
|
978
|
+
{ key: row.code },
|
|
979
|
+
React.createElement('td', null, row.username ? `@${row.username}` : '—'),
|
|
980
|
+
React.createElement('td', null, `ID: ${row.userId}`),
|
|
981
|
+
React.createElement('td', { style: { fontFamily: 'monospace', fontWeight: 600 } }, row.code),
|
|
982
|
+
React.createElement(
|
|
983
|
+
'td',
|
|
984
|
+
{ style: { textAlign: 'right' } },
|
|
985
|
+
React.createElement(
|
|
986
|
+
'button',
|
|
987
|
+
{
|
|
988
|
+
type: 'button',
|
|
989
|
+
className: 'msgw-btn msgw-btn-mini msgw-btn-primary',
|
|
990
|
+
style: { marginRight: '6px' },
|
|
991
|
+
disabled: !!busy,
|
|
992
|
+
onClick: async () => {
|
|
993
|
+
setBusy('approve')
|
|
994
|
+
setErr('')
|
|
995
|
+
try {
|
|
996
|
+
const res = await fetch(`${ROUTE_PREFIX}/pairing/approve`, {
|
|
997
|
+
method: 'POST',
|
|
998
|
+
credentials: 'same-origin',
|
|
999
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1000
|
+
body: JSON.stringify({ code: row.code }),
|
|
1001
|
+
})
|
|
1002
|
+
const data = await res.json()
|
|
1003
|
+
if (!res.ok || !data.ok) throw new Error(data.error || res.status)
|
|
1004
|
+
await loadPairing()
|
|
1005
|
+
} catch (e) {
|
|
1006
|
+
setErr(String(e.message || e))
|
|
1007
|
+
} finally {
|
|
1008
|
+
setBusy('')
|
|
1009
|
+
}
|
|
1010
|
+
},
|
|
1011
|
+
},
|
|
1012
|
+
t('pairingApprove')
|
|
1013
|
+
),
|
|
1014
|
+
React.createElement(
|
|
1015
|
+
'button',
|
|
1016
|
+
{
|
|
1017
|
+
type: 'button',
|
|
1018
|
+
className: 'msgw-btn msgw-btn-mini msgw-btn-danger',
|
|
1019
|
+
disabled: !!busy,
|
|
1020
|
+
onClick: async () => {
|
|
1021
|
+
setBusy('reject')
|
|
1022
|
+
setErr('')
|
|
1023
|
+
try {
|
|
1024
|
+
const res = await fetch(`${ROUTE_PREFIX}/pairing/reject`, {
|
|
1025
|
+
method: 'POST',
|
|
1026
|
+
credentials: 'same-origin',
|
|
1027
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1028
|
+
body: JSON.stringify({ code: row.code }),
|
|
1029
|
+
})
|
|
1030
|
+
const data = await res.json()
|
|
1031
|
+
if (!res.ok || !data.ok) throw new Error(data.error || res.status)
|
|
1032
|
+
await loadPairing()
|
|
1033
|
+
} catch (e) {
|
|
1034
|
+
setErr(String(e.message || e))
|
|
1035
|
+
} finally {
|
|
1036
|
+
setBusy('')
|
|
1037
|
+
}
|
|
1038
|
+
},
|
|
1039
|
+
},
|
|
1040
|
+
t('pairingReject')
|
|
1041
|
+
)
|
|
1042
|
+
)
|
|
1043
|
+
)
|
|
1044
|
+
)
|
|
1045
|
+
)
|
|
1046
|
+
)
|
|
1047
|
+
),
|
|
1048
|
+
|
|
1049
|
+
// Card 7: Advanced Connection & Webhook
|
|
1050
|
+
React.createElement(
|
|
1051
|
+
'div',
|
|
1052
|
+
{ className: 'msgw-section-card' },
|
|
1053
|
+
React.createElement(
|
|
1054
|
+
'button',
|
|
1055
|
+
{
|
|
1056
|
+
type: 'button',
|
|
1057
|
+
className: 'msgw-btn',
|
|
1058
|
+
style: { width: '100%', justifyContent: 'space-between' },
|
|
1059
|
+
onClick: () => setShowAdvanced((v) => !v),
|
|
1060
|
+
},
|
|
1061
|
+
React.createElement('span', null, showAdvanced ? t('hideAdvanced') : t('showAdvanced')),
|
|
1062
|
+
React.createElement('span', { style: { transform: showAdvanced ? 'rotate(180deg)' : 'none', transition: 'transform .16s' } },
|
|
1063
|
+
React.createElement(Chevron)
|
|
1064
|
+
)
|
|
1065
|
+
),
|
|
1066
|
+
showAdvanced ? React.createElement(
|
|
1067
|
+
'div',
|
|
1068
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: '12px', marginTop: '12px' } },
|
|
1069
|
+
React.createElement(Field, { label: t('transport') },
|
|
1070
|
+
React.createElement(
|
|
1071
|
+
'select',
|
|
1072
|
+
{
|
|
1073
|
+
className: 'msgw-select',
|
|
1074
|
+
value: cfg.telegram?.transport || 'poll',
|
|
1075
|
+
onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, transport: e.target.value } }),
|
|
1076
|
+
},
|
|
1077
|
+
React.createElement('option', { value: 'poll' }, t('transportPoll')),
|
|
1078
|
+
React.createElement('option', { value: 'webhook' }, t('transportWebhook'))
|
|
1079
|
+
)
|
|
1080
|
+
),
|
|
1081
|
+
React.createElement(Field, { label: t('webhookUrl'), hint: t('webhookUrlHint') },
|
|
1082
|
+
React.createElement('input', {
|
|
1083
|
+
className: 'msgw-input',
|
|
1084
|
+
value: cfg.telegram?.webhookUrl || '',
|
|
1085
|
+
onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, webhookUrl: e.target.value } }),
|
|
1086
|
+
})
|
|
1087
|
+
),
|
|
1088
|
+
React.createElement(Field, { label: t('webhookSecret'), hint: t('webhookSecretHint') },
|
|
1089
|
+
React.createElement('input', {
|
|
1090
|
+
type: 'password',
|
|
1091
|
+
className: 'msgw-input',
|
|
1092
|
+
value: webhookSecret,
|
|
1093
|
+
placeholder: cfg.telegram?.webhookSecretConfigured ? '••••••••••••••••' : '',
|
|
1094
|
+
onChange: (e) => setWebhookSecret(e.target.value),
|
|
1095
|
+
})
|
|
1096
|
+
),
|
|
1097
|
+
React.createElement(Field, { label: t('pollTimeout') },
|
|
1098
|
+
React.createElement('input', {
|
|
1099
|
+
type: 'number',
|
|
1100
|
+
className: 'msgw-input',
|
|
1101
|
+
min: 10,
|
|
1102
|
+
max: 60,
|
|
1103
|
+
value: cfg.telegram?.pollTimeoutSeconds ?? 50,
|
|
1104
|
+
onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, pollTimeoutSeconds: Number(e.target.value) || 50 } }),
|
|
1105
|
+
})
|
|
1106
|
+
),
|
|
1107
|
+
React.createElement(Field, { label: t('pollInterval') },
|
|
1108
|
+
React.createElement('input', {
|
|
1109
|
+
type: 'number',
|
|
1110
|
+
className: 'msgw-input',
|
|
1111
|
+
min: 100,
|
|
1112
|
+
max: 5000,
|
|
1113
|
+
value: cfg.telegram?.pollIntervalMs ?? 500,
|
|
1114
|
+
onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, pollIntervalMs: Number(e.target.value) || 500 } }),
|
|
1115
|
+
})
|
|
1116
|
+
),
|
|
1117
|
+
React.createElement(Field, { label: t('idleTimeout') },
|
|
1118
|
+
React.createElement('input', {
|
|
1119
|
+
type: 'number',
|
|
1120
|
+
className: 'msgw-input',
|
|
1121
|
+
min: 300,
|
|
1122
|
+
max: 86400,
|
|
1123
|
+
value: Math.round((cfg.agent?.idleTimeoutMs ?? 3600000) / 1000),
|
|
1124
|
+
onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, idleTimeoutMs: (Number(e.target.value) || 3600) * 1000 } }),
|
|
1125
|
+
})
|
|
1126
|
+
)
|
|
1127
|
+
) : null
|
|
1128
|
+
),
|
|
1129
|
+
|
|
1130
|
+
// Sticky Footer
|
|
1131
|
+
React.createElement(
|
|
1132
|
+
'div',
|
|
1133
|
+
{
|
|
1134
|
+
className: 'msgw-foot',
|
|
1135
|
+
style: {
|
|
1136
|
+
display: 'flex',
|
|
1137
|
+
justifyContent: 'flex-end',
|
|
1138
|
+
gap: '10px',
|
|
1139
|
+
paddingTop: '12px',
|
|
1140
|
+
borderTop: '1px solid var(--dsw-alias-border-l2)',
|
|
1141
|
+
},
|
|
1142
|
+
},
|
|
1143
|
+
React.createElement(
|
|
1144
|
+
'button',
|
|
1145
|
+
{
|
|
1146
|
+
type: 'button',
|
|
1147
|
+
className: 'msgw-btn msgw-btn-primary',
|
|
1148
|
+
disabled: !!busy,
|
|
1149
|
+
onClick: () => save({}),
|
|
1150
|
+
},
|
|
1151
|
+
busy === 'save' ? t('saving') : t('save')
|
|
1152
|
+
)
|
|
1153
|
+
)
|
|
1154
|
+
)
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
function PluginCard(props) {
|
|
1158
|
+
const [open, setOpen] = React.useState(false)
|
|
1159
|
+
const t = props?.t || makeT(props?.locale)
|
|
1160
|
+
React.useEffect(() => {
|
|
1161
|
+
ensureCss()
|
|
1162
|
+
}, [])
|
|
1163
|
+
|
|
1164
|
+
return React.createElement(
|
|
1165
|
+
'li',
|
|
1166
|
+
{ className: 'msgw-card msgw-section-card', style: { listStyle: 'none', marginBottom: '12px' } },
|
|
1167
|
+
React.createElement(
|
|
1168
|
+
'button',
|
|
1169
|
+
{
|
|
1170
|
+
type: 'button',
|
|
1171
|
+
className: 'msgw-head',
|
|
1172
|
+
style: {
|
|
1173
|
+
background: 'none',
|
|
1174
|
+
border: 'none',
|
|
1175
|
+
cursor: 'pointer',
|
|
1176
|
+
display: 'flex',
|
|
1177
|
+
alignItems: 'center',
|
|
1178
|
+
width: '100%',
|
|
1179
|
+
padding: 0,
|
|
1180
|
+
textAlign: 'left',
|
|
1181
|
+
},
|
|
1182
|
+
'aria-expanded': open,
|
|
1183
|
+
onClick: () => setOpen((v) => !v),
|
|
1184
|
+
},
|
|
1185
|
+
React.createElement(
|
|
1186
|
+
'div',
|
|
1187
|
+
{ className: 'msgw-headText', style: { flex: 1 } },
|
|
1188
|
+
React.createElement('div', { className: 'msgw-title', style: { fontWeight: 600, fontSize: '15px' } }, t('title')),
|
|
1189
|
+
React.createElement('div', { className: 'msgw-sub', style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('description'))
|
|
1190
|
+
),
|
|
1191
|
+
React.createElement(
|
|
1192
|
+
'span',
|
|
1193
|
+
{ className: 'msgw-chev', style: { transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .16s' } },
|
|
1194
|
+
React.createElement(Chevron)
|
|
1195
|
+
)
|
|
1196
|
+
),
|
|
1197
|
+
open
|
|
1198
|
+
? React.createElement(
|
|
1199
|
+
'div',
|
|
1200
|
+
{ className: 'msgw-body', style: { marginTop: '16px' } },
|
|
1201
|
+
React.createElement(
|
|
1202
|
+
ErrorBoundary,
|
|
1203
|
+
null,
|
|
1204
|
+
React.createElement(SettingsPage, { ...props, ctx: (props && props.ctx) || ctx, t })
|
|
1205
|
+
)
|
|
1206
|
+
)
|
|
1207
|
+
: null
|
|
1208
|
+
)
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
function refreshMirrorUntilVisible(ctx) {
|
|
1212
|
+
const visible = () => {
|
|
1213
|
+
try {
|
|
1214
|
+
const s = (ctx?.get && ctx.get('lanSettings')) || (ctx?.get && ctx.get('settingsScope')) || ctx?.settingsScope
|
|
1215
|
+
const view = s?.describe?.()?.getSnapshot?.()?.view
|
|
1216
|
+
return !!view && Array.isArray(view.namespaces) && view.namespaces.some((row) => row.ns === NS)
|
|
1217
|
+
} catch (_) {
|
|
1218
|
+
return false
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
if (visible()) return () => {}
|
|
1222
|
+
let tries = 0
|
|
1223
|
+
const timer = setInterval(() => {
|
|
1224
|
+
if (visible() || tries >= 15) { clearInterval(timer); return }
|
|
1225
|
+
tries += 1
|
|
1226
|
+
try {
|
|
1227
|
+
const s = (ctx?.get && ctx.get('lanSettings')) || (ctx?.get && ctx.get('settingsScope')) || ctx?.settingsScope
|
|
1228
|
+
s?.describe?.()?.load?.()
|
|
1229
|
+
} catch (_) {}
|
|
1230
|
+
}, 1000)
|
|
1231
|
+
return () => clearInterval(timer)
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
function apply(ctx) {
|
|
1235
|
+
try {
|
|
1236
|
+
if (typeof ctx.effect === 'function') {
|
|
1237
|
+
ctx.effect(() => {
|
|
1238
|
+
try {
|
|
1239
|
+
const loc = ctx?.get?.('locale') || ctx?.locale
|
|
1240
|
+
if (loc?.register) {
|
|
1241
|
+
loc.register(NS, { en, ru })
|
|
1242
|
+
}
|
|
1243
|
+
} catch (taken) {
|
|
1244
|
+
console.warn('[dsh-messenger-gateway] словарь уже зарегистрирован:', taken && taken.message)
|
|
1245
|
+
}
|
|
1246
|
+
}, 'dsh-messenger-gateway: dictionaries')
|
|
1247
|
+
} else {
|
|
1248
|
+
const loc = ctx?.get?.('locale') || ctx?.locale
|
|
1249
|
+
if (loc?.register) {
|
|
1250
|
+
loc.register(NS, { en, ru })
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
} catch (err) {
|
|
1254
|
+
console.warn('[dsh-messenger-gateway] ошибка регистрации словаря:', err && err.message)
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
if (typeof ctx.effect === 'function') {
|
|
1258
|
+
ctx.effect(
|
|
1259
|
+
() => refreshMirrorUntilVisible(ctx),
|
|
1260
|
+
'dsh-messenger-gateway: re-read the settings mirror until our namespace appears',
|
|
1261
|
+
)
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
function registerSlotWhenReady(slotName, registerFn) {
|
|
1265
|
+
const slots = ctx?.get?.('slots') || ctx?.slots
|
|
1266
|
+
if (!slots) return
|
|
1267
|
+
if (typeof slots.inject === 'function') {
|
|
1268
|
+
try {
|
|
1269
|
+
slots.inject(slotName, () => {
|
|
1270
|
+
try {
|
|
1271
|
+
return registerFn()
|
|
1272
|
+
} catch (err) {
|
|
1273
|
+
console.warn('[dsh-messenger-gateway] Error registering slot ' + slotName + ':', err)
|
|
1274
|
+
}
|
|
1275
|
+
})
|
|
1276
|
+
return
|
|
1277
|
+
} catch (err) {
|
|
1278
|
+
console.warn('[dsh-messenger-gateway] Failed to inject slot ' + slotName + ':', err)
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
if (typeof slots.register === 'function') {
|
|
1282
|
+
try {
|
|
1283
|
+
registerFn()
|
|
1284
|
+
} catch (err) {
|
|
1285
|
+
console.warn('[dsh-messenger-gateway] Failed direct registration for ' + slotName + ':', err)
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
registerSlotWhenReady('settings.plugin.item', () => {
|
|
1291
|
+
const slots = ctx?.get?.('slots') || ctx?.slots
|
|
1292
|
+
return slots.register(
|
|
1293
|
+
{
|
|
1294
|
+
name: 'settings.plugin.item',
|
|
1295
|
+
key: NS,
|
|
1296
|
+
locale: NS,
|
|
1297
|
+
inject: () => ({ ctx }),
|
|
1298
|
+
},
|
|
1299
|
+
(props) => React.createElement(ErrorBoundary, null, React.createElement(PluginCard, { ...props, ctx: (props && props.ctx) || ctx }))
|
|
1300
|
+
)
|
|
1301
|
+
})
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
module.exports = { apply, inject: ['slots', 'locale', 'settingsScope'] }
|
|
1305
|
+
return module.exports
|
|
1306
|
+
},
|
|
1307
|
+
})
|