@leeoohoo/ui-apps-devkit 0.1.0 → 0.1.1
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 +76 -70
- package/bin/chatos-uiapp.js +4 -4
- package/package.json +25 -22
- package/src/cli.js +53 -53
- package/src/commands/dev.js +14 -14
- package/src/commands/init.js +142 -141
- package/src/commands/install.js +55 -55
- package/src/commands/pack.js +72 -72
- package/src/commands/validate.js +113 -103
- package/src/lib/args.js +49 -49
- package/src/lib/config.js +29 -29
- package/src/lib/fs.js +78 -78
- package/src/lib/path-boundary.js +16 -16
- package/src/lib/plugin.js +45 -45
- package/src/lib/template.js +172 -168
- package/src/sandbox/server.js +1200 -861
- package/templates/basic/README.md +77 -58
- package/templates/basic/chatos.config.json +5 -5
- package/templates/basic/docs/CHATOS_UI_APPS_AI_CONTRIBUTIONS.md +194 -163
- package/templates/basic/docs/CHATOS_UI_APPS_BACKEND_PROTOCOL.md +74 -74
- package/templates/basic/docs/CHATOS_UI_APPS_HOST_API.md +136 -123
- package/templates/basic/docs/CHATOS_UI_APPS_OVERVIEW.md +113 -110
- package/templates/basic/docs/CHATOS_UI_APPS_PLUGIN_MANIFEST.md +226 -212
- package/templates/basic/docs/CHATOS_UI_APPS_STYLE_GUIDE.md +95 -0
- package/templates/basic/docs/CHATOS_UI_APPS_TROUBLESHOOTING.md +45 -0
- package/templates/basic/docs/CHATOS_UI_PROMPTS_PROTOCOL.md +392 -392
- package/templates/basic/plugin/apps/app/compact.mjs +41 -0
- package/templates/basic/plugin/apps/app/index.mjs +287 -263
- package/templates/basic/plugin/apps/app/mcp-prompt.en.md +7 -7
- package/templates/basic/plugin/apps/app/mcp-prompt.zh.md +7 -7
- package/templates/basic/plugin/apps/app/mcp-server.mjs +15 -15
- package/templates/basic/plugin/backend/index.mjs +37 -37
- package/templates/basic/template.json +7 -7
- package/templates/notepad/README.md +58 -36
- package/templates/notepad/chatos.config.json +4 -4
- package/templates/notepad/docs/CHATOS_UI_APPS_AI_CONTRIBUTIONS.md +194 -163
- package/templates/notepad/docs/CHATOS_UI_APPS_BACKEND_PROTOCOL.md +74 -74
- package/templates/notepad/docs/CHATOS_UI_APPS_HOST_API.md +136 -123
- package/templates/notepad/docs/CHATOS_UI_APPS_OVERVIEW.md +113 -110
- package/templates/notepad/docs/CHATOS_UI_APPS_PLUGIN_MANIFEST.md +226 -212
- package/templates/notepad/docs/CHATOS_UI_APPS_STYLE_GUIDE.md +95 -0
- package/templates/notepad/docs/CHATOS_UI_APPS_TROUBLESHOOTING.md +45 -0
- package/templates/notepad/docs/CHATOS_UI_PROMPTS_PROTOCOL.md +392 -392
- package/templates/notepad/plugin/apps/app/api.mjs +30 -30
- package/templates/notepad/plugin/apps/app/compact.mjs +41 -0
- package/templates/notepad/plugin/apps/app/dom.mjs +14 -14
- package/templates/notepad/plugin/apps/app/ds-tree.mjs +35 -35
- package/templates/notepad/plugin/apps/app/index.mjs +1056 -1056
- package/templates/notepad/plugin/apps/app/layers.mjs +338 -338
- package/templates/notepad/plugin/apps/app/markdown.mjs +120 -120
- package/templates/notepad/plugin/apps/app/mcp-prompt.en.md +22 -22
- package/templates/notepad/plugin/apps/app/mcp-prompt.zh.md +22 -22
- package/templates/notepad/plugin/apps/app/mcp-server.mjs +200 -200
- package/templates/notepad/plugin/apps/app/styles.mjs +355 -355
- package/templates/notepad/plugin/apps/app/tags.mjs +21 -21
- package/templates/notepad/plugin/apps/app/ui.mjs +280 -280
- package/templates/notepad/plugin/backend/index.mjs +99 -99
- package/templates/notepad/plugin/plugin.json +23 -23
- package/templates/notepad/plugin/shared/notepad-paths.mjs +62 -62
- package/templates/notepad/plugin/shared/notepad-store.mjs +765 -765
- package/templates/notepad/template.json +8 -8
package/src/sandbox/server.js
CHANGED
|
@@ -1,861 +1,1200 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import http from 'http';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import url from 'url';
|
|
5
|
-
|
|
6
|
-
import { ensureDir, isDirectory, isFile } from '../lib/fs.js';
|
|
7
|
-
import { loadPluginManifest, pickAppFromManifest } from '../lib/plugin.js';
|
|
8
|
-
import { resolveInsideDir } from '../lib/path-boundary.js';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
function
|
|
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
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
const
|
|
363
|
-
|
|
364
|
-
const
|
|
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
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
630
|
-
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
const
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import http from 'http';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import url from 'url';
|
|
5
|
+
|
|
6
|
+
import { ensureDir, isDirectory, isFile } from '../lib/fs.js';
|
|
7
|
+
import { loadPluginManifest, pickAppFromManifest } from '../lib/plugin.js';
|
|
8
|
+
import { resolveInsideDir } from '../lib/path-boundary.js';
|
|
9
|
+
|
|
10
|
+
const __filename = url.fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
|
|
13
|
+
const TOKEN_REGEX = /--ds-[a-z0-9-]+/gi;
|
|
14
|
+
const GLOBAL_STYLES_CANDIDATES = [
|
|
15
|
+
path.resolve(__dirname, '..', '..', '..', 'common', 'aide-ui', 'components', 'GlobalStyles.jsx'),
|
|
16
|
+
path.resolve(process.cwd(), 'common', 'aide-ui', 'components', 'GlobalStyles.jsx'),
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
function loadTokenNames() {
|
|
20
|
+
for (const candidate of GLOBAL_STYLES_CANDIDATES) {
|
|
21
|
+
try {
|
|
22
|
+
if (!isFile(candidate)) continue;
|
|
23
|
+
const raw = fs.readFileSync(candidate, 'utf8');
|
|
24
|
+
const matches = raw.match(TOKEN_REGEX) || [];
|
|
25
|
+
const names = Array.from(new Set(matches.map((v) => v.toLowerCase())));
|
|
26
|
+
if (names.length > 0) return names.sort();
|
|
27
|
+
} catch {
|
|
28
|
+
// ignore
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
function sendJson(res, status, obj) {
|
|
36
|
+
const raw = JSON.stringify(obj);
|
|
37
|
+
res.writeHead(status, {
|
|
38
|
+
'content-type': 'application/json; charset=utf-8',
|
|
39
|
+
'cache-control': 'no-store',
|
|
40
|
+
});
|
|
41
|
+
res.end(raw);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function sendText(res, status, text, contentType) {
|
|
45
|
+
res.writeHead(status, {
|
|
46
|
+
'content-type': contentType || 'text/plain; charset=utf-8',
|
|
47
|
+
'cache-control': 'no-store',
|
|
48
|
+
});
|
|
49
|
+
res.end(text);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function guessContentType(filePath) {
|
|
53
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
54
|
+
if (ext === '.html') return 'text/html; charset=utf-8';
|
|
55
|
+
if (ext === '.css') return 'text/css; charset=utf-8';
|
|
56
|
+
if (ext === '.mjs' || ext === '.js') return 'text/javascript; charset=utf-8';
|
|
57
|
+
if (ext === '.json') return 'application/json; charset=utf-8';
|
|
58
|
+
if (ext === '.md') return 'text/markdown; charset=utf-8';
|
|
59
|
+
if (ext === '.svg') return 'image/svg+xml';
|
|
60
|
+
if (ext === '.png') return 'image/png';
|
|
61
|
+
return 'application/octet-stream';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function serveStaticFile(res, filePath) {
|
|
65
|
+
if (!isFile(filePath)) return false;
|
|
66
|
+
const ct = guessContentType(filePath);
|
|
67
|
+
const buf = fs.readFileSync(filePath);
|
|
68
|
+
res.writeHead(200, { 'content-type': ct, 'cache-control': 'no-store' });
|
|
69
|
+
res.end(buf);
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function startRecursiveWatcher(rootDir, onChange) {
|
|
74
|
+
const root = path.resolve(rootDir);
|
|
75
|
+
if (!isDirectory(root)) return () => {};
|
|
76
|
+
|
|
77
|
+
const watchers = new Map();
|
|
78
|
+
|
|
79
|
+
const shouldIgnore = (p) => {
|
|
80
|
+
const base = path.basename(p);
|
|
81
|
+
if (!base) return false;
|
|
82
|
+
if (base === 'node_modules') return true;
|
|
83
|
+
if (base === '.git') return true;
|
|
84
|
+
if (base === '.DS_Store') return true;
|
|
85
|
+
return false;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const scan = (dir) => {
|
|
89
|
+
const abs = path.resolve(dir);
|
|
90
|
+
if (!isDirectory(abs)) return;
|
|
91
|
+
if (shouldIgnore(abs)) return;
|
|
92
|
+
if (!watchers.has(abs)) {
|
|
93
|
+
try {
|
|
94
|
+
const w = fs.watch(abs, (eventType, filename) => {
|
|
95
|
+
const relName = filename ? String(filename) : '';
|
|
96
|
+
const filePath = relName ? path.join(abs, relName) : abs;
|
|
97
|
+
try {
|
|
98
|
+
onChange({ eventType, filePath });
|
|
99
|
+
} catch {
|
|
100
|
+
// ignore
|
|
101
|
+
}
|
|
102
|
+
scheduleRescan();
|
|
103
|
+
});
|
|
104
|
+
watchers.set(abs, w);
|
|
105
|
+
} catch {
|
|
106
|
+
// ignore
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let entries = [];
|
|
111
|
+
try {
|
|
112
|
+
entries = fs.readdirSync(abs, { withFileTypes: true });
|
|
113
|
+
} catch {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
for (const ent of entries) {
|
|
117
|
+
if (!ent?.isDirectory?.()) continue;
|
|
118
|
+
const child = path.join(abs, ent.name);
|
|
119
|
+
if (shouldIgnore(child)) continue;
|
|
120
|
+
scan(child);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
let rescanTimer = null;
|
|
125
|
+
const scheduleRescan = () => {
|
|
126
|
+
if (rescanTimer) return;
|
|
127
|
+
rescanTimer = setTimeout(() => {
|
|
128
|
+
rescanTimer = null;
|
|
129
|
+
scan(root);
|
|
130
|
+
}, 250);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
scan(root);
|
|
134
|
+
|
|
135
|
+
return () => {
|
|
136
|
+
if (rescanTimer) {
|
|
137
|
+
try {
|
|
138
|
+
clearTimeout(rescanTimer);
|
|
139
|
+
} catch {
|
|
140
|
+
// ignore
|
|
141
|
+
}
|
|
142
|
+
rescanTimer = null;
|
|
143
|
+
}
|
|
144
|
+
for (const w of watchers.values()) {
|
|
145
|
+
try {
|
|
146
|
+
w.close();
|
|
147
|
+
} catch {
|
|
148
|
+
// ignore
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
watchers.clear();
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function htmlPage() {
|
|
156
|
+
return `<!doctype html>
|
|
157
|
+
<html lang="zh-CN">
|
|
158
|
+
<head>
|
|
159
|
+
<meta charset="UTF-8" />
|
|
160
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
161
|
+
<title>ChatOS UI Apps Sandbox</title>
|
|
162
|
+
<style>
|
|
163
|
+
:root {
|
|
164
|
+
color-scheme: light;
|
|
165
|
+
--ds-accent: #00d4ff;
|
|
166
|
+
--ds-accent-2: #7c3aed;
|
|
167
|
+
--ds-panel-bg: rgba(255, 255, 255, 0.86);
|
|
168
|
+
--ds-panel-border: rgba(15, 23, 42, 0.08);
|
|
169
|
+
--ds-subtle-bg: rgba(255, 255, 255, 0.62);
|
|
170
|
+
--ds-selected-bg: linear-gradient(90deg, rgba(0, 212, 255, 0.14), rgba(124, 58, 237, 0.08));
|
|
171
|
+
--ds-focus-ring: rgba(0, 212, 255, 0.32);
|
|
172
|
+
--ds-nav-hover-bg: rgba(15, 23, 42, 0.06);
|
|
173
|
+
--ds-code-bg: #f7f9fb;
|
|
174
|
+
--ds-code-border: #eef2f7;
|
|
175
|
+
--sandbox-bg: #f5f7fb;
|
|
176
|
+
--sandbox-text: #111;
|
|
177
|
+
}
|
|
178
|
+
:root[data-theme='dark'] {
|
|
179
|
+
color-scheme: dark;
|
|
180
|
+
--ds-accent: #00d4ff;
|
|
181
|
+
--ds-accent-2: #a855f7;
|
|
182
|
+
--ds-panel-bg: rgba(17, 19, 28, 0.82);
|
|
183
|
+
--ds-panel-border: rgba(255, 255, 255, 0.14);
|
|
184
|
+
--ds-subtle-bg: rgba(255, 255, 255, 0.04);
|
|
185
|
+
--ds-selected-bg: linear-gradient(90deg, rgba(0, 212, 255, 0.18), rgba(168, 85, 247, 0.14));
|
|
186
|
+
--ds-focus-ring: rgba(0, 212, 255, 0.5);
|
|
187
|
+
--ds-nav-hover-bg: rgba(255, 255, 255, 0.08);
|
|
188
|
+
--ds-code-bg: #0d1117;
|
|
189
|
+
--ds-code-border: #30363d;
|
|
190
|
+
--sandbox-bg: #0f1115;
|
|
191
|
+
--sandbox-text: #eee;
|
|
192
|
+
}
|
|
193
|
+
body {
|
|
194
|
+
margin:0;
|
|
195
|
+
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial;
|
|
196
|
+
background: var(--sandbox-bg);
|
|
197
|
+
color: var(--sandbox-text);
|
|
198
|
+
}
|
|
199
|
+
#appRoot { height: 100vh; display:flex; flex-direction:column; }
|
|
200
|
+
#sandboxToolbar {
|
|
201
|
+
flex: 0 0 auto;
|
|
202
|
+
border-bottom: 1px solid var(--ds-panel-border);
|
|
203
|
+
padding: 10px 12px;
|
|
204
|
+
background: var(--ds-panel-bg);
|
|
205
|
+
}
|
|
206
|
+
#headerSlot {
|
|
207
|
+
flex: 0 0 auto;
|
|
208
|
+
border-bottom: 1px solid var(--ds-panel-border);
|
|
209
|
+
padding: 10px 12px;
|
|
210
|
+
background: var(--ds-panel-bg);
|
|
211
|
+
}
|
|
212
|
+
#container { flex: 1 1 auto; min-height:0; overflow:hidden; }
|
|
213
|
+
#containerInner { height:100%; overflow:auto; }
|
|
214
|
+
.muted { opacity: 0.7; font-size: 12px; }
|
|
215
|
+
.bar { display:flex; gap:10px; align-items:center; justify-content:space-between; }
|
|
216
|
+
.btn {
|
|
217
|
+
border:1px solid var(--ds-panel-border);
|
|
218
|
+
background: var(--ds-subtle-bg);
|
|
219
|
+
padding:6px 10px;
|
|
220
|
+
border-radius:10px;
|
|
221
|
+
cursor:pointer;
|
|
222
|
+
font-weight:650;
|
|
223
|
+
color: inherit;
|
|
224
|
+
}
|
|
225
|
+
.btn[data-active='1'] {
|
|
226
|
+
background: var(--ds-selected-bg);
|
|
227
|
+
box-shadow: 0 0 0 2px var(--ds-focus-ring);
|
|
228
|
+
}
|
|
229
|
+
.btn:active { transform: translateY(1px); }
|
|
230
|
+
#promptsPanel {
|
|
231
|
+
position: fixed;
|
|
232
|
+
right: 12px;
|
|
233
|
+
bottom: 12px;
|
|
234
|
+
width: 420px;
|
|
235
|
+
max-height: 70vh;
|
|
236
|
+
display:none;
|
|
237
|
+
flex-direction:column;
|
|
238
|
+
background: var(--ds-panel-bg);
|
|
239
|
+
color: inherit;
|
|
240
|
+
border:1px solid var(--ds-panel-border);
|
|
241
|
+
border-radius:14px;
|
|
242
|
+
overflow:hidden;
|
|
243
|
+
box-shadow: 0 18px 60px rgba(0,0,0,0.18);
|
|
244
|
+
}
|
|
245
|
+
#promptsPanelHeader { padding: 10px 12px; display:flex; align-items:center; justify-content:space-between; border-bottom: 1px solid var(--ds-panel-border); }
|
|
246
|
+
#promptsPanelBody { padding: 10px 12px; overflow:auto; display:flex; flex-direction:column; gap:10px; }
|
|
247
|
+
#promptsFab { position: fixed; right: 16px; bottom: 16px; width: 44px; height: 44px; border-radius: 999px; display:flex; align-items:center; justify-content:center; }
|
|
248
|
+
.card { border: 1px solid var(--ds-panel-border); border-radius: 12px; padding: 10px; background: var(--ds-panel-bg); }
|
|
249
|
+
.row { display:flex; gap:10px; }
|
|
250
|
+
.toolbar-group { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
|
|
251
|
+
.segmented { display:flex; gap:6px; align-items:center; }
|
|
252
|
+
#sandboxInspector {
|
|
253
|
+
position: fixed;
|
|
254
|
+
right: 12px;
|
|
255
|
+
top: 72px;
|
|
256
|
+
width: 360px;
|
|
257
|
+
max-height: 70vh;
|
|
258
|
+
display: none;
|
|
259
|
+
flex-direction: column;
|
|
260
|
+
background: var(--ds-panel-bg);
|
|
261
|
+
border: 1px solid var(--ds-panel-border);
|
|
262
|
+
border-radius: 12px;
|
|
263
|
+
overflow: hidden;
|
|
264
|
+
box-shadow: 0 14px 40px rgba(0,0,0,0.16);
|
|
265
|
+
z-index: 10;
|
|
266
|
+
}
|
|
267
|
+
#sandboxInspectorHeader {
|
|
268
|
+
padding: 10px 12px;
|
|
269
|
+
display:flex;
|
|
270
|
+
align-items:center;
|
|
271
|
+
justify-content: space-between;
|
|
272
|
+
border-bottom: 1px solid var(--ds-panel-border);
|
|
273
|
+
}
|
|
274
|
+
#sandboxInspectorBody {
|
|
275
|
+
padding: 10px 12px;
|
|
276
|
+
overflow: auto;
|
|
277
|
+
display: flex;
|
|
278
|
+
flex-direction: column;
|
|
279
|
+
gap: 10px;
|
|
280
|
+
}
|
|
281
|
+
.section-title { font-size: 12px; font-weight: 700; opacity: 0.8; }
|
|
282
|
+
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 12px; white-space: pre-wrap; }
|
|
283
|
+
input, textarea, select {
|
|
284
|
+
width:100%;
|
|
285
|
+
padding:8px;
|
|
286
|
+
border-radius:10px;
|
|
287
|
+
border:1px solid var(--ds-panel-border);
|
|
288
|
+
background: var(--ds-subtle-bg);
|
|
289
|
+
color: inherit;
|
|
290
|
+
}
|
|
291
|
+
textarea { min-height: 70px; resize: vertical; }
|
|
292
|
+
label { font-size: 12px; opacity: 0.8; }
|
|
293
|
+
.danger { border-color: rgba(255,0,0,0.35); }
|
|
294
|
+
</style>
|
|
295
|
+
</head>
|
|
296
|
+
<body>
|
|
297
|
+
<div id="appRoot">
|
|
298
|
+
<div id="sandboxToolbar">
|
|
299
|
+
<div class="bar">
|
|
300
|
+
<div>
|
|
301
|
+
<div style="font-weight:800">ChatOS UI Apps Sandbox</div>
|
|
302
|
+
<div class="muted">Host API mock · 模拟 module mount({ container, host, slots })</div>
|
|
303
|
+
</div>
|
|
304
|
+
<div class="row toolbar-group">
|
|
305
|
+
<span class="muted">Theme</span>
|
|
306
|
+
<div class="segmented" role="group" aria-label="Theme">
|
|
307
|
+
<button id="btnThemeLight" class="btn" type="button">Light</button>
|
|
308
|
+
<button id="btnThemeDark" class="btn" type="button">Dark</button>
|
|
309
|
+
<button id="btnThemeSystem" class="btn" type="button">System</button>
|
|
310
|
+
</div>
|
|
311
|
+
<div id="themeStatus" class="muted"></div>
|
|
312
|
+
<div id="sandboxContext" class="muted"></div>
|
|
313
|
+
<button id="btnInspectorToggle" class="btn" type="button">Inspect</button>
|
|
314
|
+
<button id="btnReload" class="btn" type="button">Reload</button>
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
<div id="headerSlot"></div>
|
|
319
|
+
<div id="container"><div id="containerInner"></div></div>
|
|
320
|
+
</div>
|
|
321
|
+
|
|
322
|
+
<button id="promptsFab" class="btn" type="button">:)</button>
|
|
323
|
+
|
|
324
|
+
<div id="promptsPanel">
|
|
325
|
+
<div id="promptsPanelHeader">
|
|
326
|
+
<div style="font-weight:800">UI Prompts</div>
|
|
327
|
+
<button id="promptsClose" class="btn" type="button">Close</button>
|
|
328
|
+
</div>
|
|
329
|
+
<div id="promptsPanelBody"></div>
|
|
330
|
+
</div>
|
|
331
|
+
|
|
332
|
+
<div id="sandboxInspector" aria-hidden="true">
|
|
333
|
+
<div id="sandboxInspectorHeader">
|
|
334
|
+
<div style="font-weight:800">Sandbox Inspector</div>
|
|
335
|
+
<div class="row">
|
|
336
|
+
<button id="btnInspectorRefresh" class="btn" type="button">Refresh</button>
|
|
337
|
+
<button id="btnInspectorClose" class="btn" type="button">Close</button>
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
<div id="sandboxInspectorBody">
|
|
341
|
+
<div>
|
|
342
|
+
<div class="section-title">Host Context</div>
|
|
343
|
+
<pre id="inspectorContext" class="mono"></pre>
|
|
344
|
+
</div>
|
|
345
|
+
<div>
|
|
346
|
+
<div class="section-title">Theme</div>
|
|
347
|
+
<pre id="inspectorTheme" class="mono"></pre>
|
|
348
|
+
</div>
|
|
349
|
+
<div>
|
|
350
|
+
<div class="section-title">Tokens</div>
|
|
351
|
+
<pre id="inspectorTokens" class="mono"></pre>
|
|
352
|
+
</div>
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
|
|
356
|
+
<script type="module" src="/sandbox.mjs"></script>
|
|
357
|
+
</body>
|
|
358
|
+
</html>`;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function sandboxClientJs() {
|
|
362
|
+
return `const $ = (sel) => document.querySelector(sel);
|
|
363
|
+
|
|
364
|
+
const container = $('#containerInner');
|
|
365
|
+
const headerSlot = $('#headerSlot');
|
|
366
|
+
const fab = $('#promptsFab');
|
|
367
|
+
const panel = $('#promptsPanel');
|
|
368
|
+
const panelBody = $('#promptsPanelBody');
|
|
369
|
+
const panelClose = $('#promptsClose');
|
|
370
|
+
const btnThemeLight = $('#btnThemeLight');
|
|
371
|
+
const btnThemeDark = $('#btnThemeDark');
|
|
372
|
+
const btnThemeSystem = $('#btnThemeSystem');
|
|
373
|
+
const themeStatus = $('#themeStatus');
|
|
374
|
+
const sandboxContext = $('#sandboxContext');
|
|
375
|
+
const btnInspectorToggle = $('#btnInspectorToggle');
|
|
376
|
+
const sandboxInspector = $('#sandboxInspector');
|
|
377
|
+
const btnInspectorClose = $('#btnInspectorClose');
|
|
378
|
+
const btnInspectorRefresh = $('#btnInspectorRefresh');
|
|
379
|
+
const inspectorContext = $('#inspectorContext');
|
|
380
|
+
const inspectorTheme = $('#inspectorTheme');
|
|
381
|
+
const inspectorTokens = $('#inspectorTokens');
|
|
382
|
+
|
|
383
|
+
const setPanelOpen = (open) => { panel.style.display = open ? 'flex' : 'none'; };
|
|
384
|
+
fab.addEventListener('click', () => setPanelOpen(panel.style.display !== 'flex'));
|
|
385
|
+
panelClose.addEventListener('click', () => setPanelOpen(false));
|
|
386
|
+
window.addEventListener('chatos:uiPrompts:open', () => setPanelOpen(true));
|
|
387
|
+
window.addEventListener('chatos:uiPrompts:close', () => setPanelOpen(false));
|
|
388
|
+
window.addEventListener('chatos:uiPrompts:toggle', () => setPanelOpen(panel.style.display !== 'flex'));
|
|
389
|
+
|
|
390
|
+
const THEME_STORAGE_KEY = 'chatos:sandbox:theme-mode';
|
|
391
|
+
const themeListeners = new Set();
|
|
392
|
+
const themeButtons = [
|
|
393
|
+
{ mode: 'light', el: btnThemeLight },
|
|
394
|
+
{ mode: 'dark', el: btnThemeDark },
|
|
395
|
+
{ mode: 'system', el: btnThemeSystem },
|
|
396
|
+
];
|
|
397
|
+
const systemQuery = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null;
|
|
398
|
+
|
|
399
|
+
const normalizeThemeMode = (mode) => (mode === 'light' || mode === 'dark' || mode === 'system' ? mode : 'system');
|
|
400
|
+
|
|
401
|
+
const loadThemeMode = () => {
|
|
402
|
+
try {
|
|
403
|
+
return normalizeThemeMode(String(localStorage.getItem(THEME_STORAGE_KEY) || ''));
|
|
404
|
+
} catch {
|
|
405
|
+
return 'system';
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
let themeMode = loadThemeMode();
|
|
410
|
+
let currentTheme = 'light';
|
|
411
|
+
let inspectorEnabled = false;
|
|
412
|
+
let inspectorTimer = null;
|
|
413
|
+
|
|
414
|
+
const resolveTheme = () => {
|
|
415
|
+
if (themeMode === 'light' || themeMode === 'dark') return themeMode;
|
|
416
|
+
return systemQuery && systemQuery.matches ? 'dark' : 'light';
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
const emitThemeChange = (theme) => {
|
|
420
|
+
for (const fn of themeListeners) { try { fn(theme); } catch {} }
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const updateThemeControls = () => {
|
|
424
|
+
for (const { mode, el } of themeButtons) {
|
|
425
|
+
if (!el) continue;
|
|
426
|
+
const active = mode === themeMode;
|
|
427
|
+
el.dataset.active = active ? '1' : '0';
|
|
428
|
+
el.setAttribute('aria-pressed', active ? 'true' : 'false');
|
|
429
|
+
}
|
|
430
|
+
if (themeStatus) {
|
|
431
|
+
themeStatus.textContent = themeMode === 'system' ? 'system -> ' + currentTheme : currentTheme;
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
const updateContextStatus = () => {
|
|
436
|
+
if (!sandboxContext) return;
|
|
437
|
+
sandboxContext.textContent = __SANDBOX__.pluginId + ':' + __SANDBOX__.appId;
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
const isInspectorOpen = () => sandboxInspector && sandboxInspector.style.display === 'flex';
|
|
441
|
+
|
|
442
|
+
const formatJson = (value) => {
|
|
443
|
+
try {
|
|
444
|
+
return JSON.stringify(value, null, 2);
|
|
445
|
+
} catch {
|
|
446
|
+
return String(value);
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
const tokenNameList = Array.isArray(__SANDBOX__.tokenNames) ? __SANDBOX__.tokenNames : [];
|
|
451
|
+
|
|
452
|
+
const collectTokens = () => {
|
|
453
|
+
const style = getComputedStyle(document.documentElement);
|
|
454
|
+
const names = new Set(tokenNameList);
|
|
455
|
+
for (let i = 0; i < style.length; i += 1) {
|
|
456
|
+
const name = style[i];
|
|
457
|
+
if (name && name.startsWith('--ds-')) names.add(name);
|
|
458
|
+
}
|
|
459
|
+
return [...names]
|
|
460
|
+
.sort()
|
|
461
|
+
.map((name) => {
|
|
462
|
+
const value = style.getPropertyValue(name).trim();
|
|
463
|
+
return name + ': ' + (value || '(unset)');
|
|
464
|
+
})
|
|
465
|
+
.join('\\n');
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
const readHostContext = () => {
|
|
469
|
+
if (!inspectorEnabled) return null;
|
|
470
|
+
if (typeof host?.context?.get === 'function') return host.context.get();
|
|
471
|
+
return { pluginId: __SANDBOX__.pluginId, appId: __SANDBOX__.appId, theme: currentTheme, bridge: { enabled: true } };
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
const readThemeInfo = () => ({
|
|
475
|
+
themeMode,
|
|
476
|
+
currentTheme,
|
|
477
|
+
dataTheme: document.documentElement.dataset.theme || '',
|
|
478
|
+
dataThemeMode: document.documentElement.dataset.themeMode || '',
|
|
479
|
+
prefersColorScheme: systemQuery ? (systemQuery.matches ? 'dark' : 'light') : 'unknown',
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
const updateInspector = () => {
|
|
483
|
+
if (!inspectorEnabled) return;
|
|
484
|
+
if (inspectorContext) inspectorContext.textContent = formatJson(readHostContext());
|
|
485
|
+
if (inspectorTheme) inspectorTheme.textContent = formatJson(readThemeInfo());
|
|
486
|
+
if (inspectorTokens) inspectorTokens.textContent = collectTokens();
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
const startInspectorTimer = () => {
|
|
490
|
+
if (inspectorTimer) return;
|
|
491
|
+
inspectorTimer = setInterval(updateInspector, 1000);
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
const stopInspectorTimer = () => {
|
|
495
|
+
if (!inspectorTimer) return;
|
|
496
|
+
clearInterval(inspectorTimer);
|
|
497
|
+
inspectorTimer = null;
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
const setInspectorOpen = (open) => {
|
|
501
|
+
if (!sandboxInspector) return;
|
|
502
|
+
sandboxInspector.style.display = open ? 'flex' : 'none';
|
|
503
|
+
sandboxInspector.setAttribute('aria-hidden', open ? 'false' : 'true');
|
|
504
|
+
if (open) {
|
|
505
|
+
updateInspector();
|
|
506
|
+
startInspectorTimer();
|
|
507
|
+
} else {
|
|
508
|
+
stopInspectorTimer();
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
const updateInspectorIfOpen = () => {
|
|
513
|
+
if (!inspectorEnabled) return;
|
|
514
|
+
if (isInspectorOpen()) updateInspector();
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
const applyThemeMode = (mode, { persist = true } = {}) => {
|
|
518
|
+
themeMode = normalizeThemeMode(mode);
|
|
519
|
+
if (persist) {
|
|
520
|
+
try {
|
|
521
|
+
localStorage.setItem(THEME_STORAGE_KEY, themeMode);
|
|
522
|
+
} catch {
|
|
523
|
+
// ignore
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const nextTheme = resolveTheme();
|
|
527
|
+
const prevTheme = currentTheme;
|
|
528
|
+
currentTheme = nextTheme;
|
|
529
|
+
document.documentElement.dataset.theme = nextTheme;
|
|
530
|
+
document.documentElement.dataset.themeMode = themeMode;
|
|
531
|
+
updateThemeControls();
|
|
532
|
+
updateInspectorIfOpen();
|
|
533
|
+
if (nextTheme !== prevTheme) emitThemeChange(nextTheme);
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
if (systemQuery && typeof systemQuery.addEventListener === 'function') {
|
|
537
|
+
systemQuery.addEventListener('change', () => {
|
|
538
|
+
if (themeMode === 'system') applyThemeMode('system', { persist: false });
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (btnThemeLight) btnThemeLight.addEventListener('click', () => applyThemeMode('light'));
|
|
543
|
+
if (btnThemeDark) btnThemeDark.addEventListener('click', () => applyThemeMode('dark'));
|
|
544
|
+
if (btnThemeSystem) btnThemeSystem.addEventListener('click', () => applyThemeMode('system'));
|
|
545
|
+
if (btnInspectorToggle) btnInspectorToggle.addEventListener('click', () => setInspectorOpen(!isInspectorOpen()));
|
|
546
|
+
if (btnInspectorClose) btnInspectorClose.addEventListener('click', () => setInspectorOpen(false));
|
|
547
|
+
if (btnInspectorRefresh) btnInspectorRefresh.addEventListener('click', () => updateInspector());
|
|
548
|
+
|
|
549
|
+
applyThemeMode(themeMode || 'system', { persist: false });
|
|
550
|
+
updateContextStatus();
|
|
551
|
+
|
|
552
|
+
const entries = [];
|
|
553
|
+
const listeners = new Set();
|
|
554
|
+
const emitUpdate = () => {
|
|
555
|
+
const payload = { path: '(sandbox)', entries: [...entries] };
|
|
556
|
+
for (const fn of listeners) { try { fn(payload); } catch {} }
|
|
557
|
+
renderPrompts();
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
const uuid = () => (globalThis.crypto?.randomUUID ? crypto.randomUUID() : String(Date.now()) + '-' + Math.random().toString(16).slice(2));
|
|
561
|
+
|
|
562
|
+
function renderPrompts() {
|
|
563
|
+
panelBody.textContent = '';
|
|
564
|
+
const pending = new Map();
|
|
565
|
+
for (const e of entries) {
|
|
566
|
+
if (e?.type !== 'ui_prompt') continue;
|
|
567
|
+
const id = String(e?.requestId || '');
|
|
568
|
+
if (!id) continue;
|
|
569
|
+
if (e.action === 'request') pending.set(id, e);
|
|
570
|
+
if (e.action === 'response') pending.delete(id);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (pending.size === 0) {
|
|
574
|
+
const empty = document.createElement('div');
|
|
575
|
+
empty.className = 'muted';
|
|
576
|
+
empty.textContent = '暂无待办(request 后会出现在这里)';
|
|
577
|
+
panelBody.appendChild(empty);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
for (const [requestId, req] of pending.entries()) {
|
|
582
|
+
const card = document.createElement('div');
|
|
583
|
+
card.className = 'card';
|
|
584
|
+
|
|
585
|
+
const title = document.createElement('div');
|
|
586
|
+
title.style.fontWeight = '800';
|
|
587
|
+
title.textContent = req?.prompt?.title || '(untitled)';
|
|
588
|
+
|
|
589
|
+
const msg = document.createElement('div');
|
|
590
|
+
msg.className = 'muted';
|
|
591
|
+
msg.style.marginTop = '6px';
|
|
592
|
+
msg.textContent = req?.prompt?.message || '';
|
|
593
|
+
|
|
594
|
+
const source = document.createElement('div');
|
|
595
|
+
source.className = 'muted';
|
|
596
|
+
source.style.marginTop = '6px';
|
|
597
|
+
source.textContent = req?.prompt?.source ? String(req.prompt.source) : '';
|
|
598
|
+
|
|
599
|
+
const form = document.createElement('div');
|
|
600
|
+
form.style.marginTop = '10px';
|
|
601
|
+
form.style.display = 'grid';
|
|
602
|
+
form.style.gap = '10px';
|
|
603
|
+
|
|
604
|
+
const kind = String(req?.prompt?.kind || '');
|
|
605
|
+
|
|
606
|
+
const mkBtn = (label, danger) => {
|
|
607
|
+
const btn = document.createElement('button');
|
|
608
|
+
btn.type = 'button';
|
|
609
|
+
btn.className = 'btn' + (danger ? ' danger' : '');
|
|
610
|
+
btn.textContent = label;
|
|
611
|
+
return btn;
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
const submit = async (response) => {
|
|
615
|
+
entries.push({ ts: new Date().toISOString(), type: 'ui_prompt', action: 'response', requestId, response });
|
|
616
|
+
emitUpdate();
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
if (kind === 'kv') {
|
|
620
|
+
const fields = Array.isArray(req?.prompt?.fields) ? req.prompt.fields : [];
|
|
621
|
+
const values = {};
|
|
622
|
+
for (const f of fields) {
|
|
623
|
+
const key = String(f?.key || '');
|
|
624
|
+
if (!key) continue;
|
|
625
|
+
const wrap = document.createElement('div');
|
|
626
|
+
const lab = document.createElement('label');
|
|
627
|
+
lab.textContent = f?.label ? String(f.label) : key;
|
|
628
|
+
const input = document.createElement(f?.multiline ? 'textarea' : 'input');
|
|
629
|
+
input.placeholder = f?.placeholder ? String(f.placeholder) : '';
|
|
630
|
+
input.value = f?.default ? String(f.default) : '';
|
|
631
|
+
input.addEventListener('input', () => { values[key] = String(input.value || ''); });
|
|
632
|
+
values[key] = String(input.value || '');
|
|
633
|
+
wrap.appendChild(lab);
|
|
634
|
+
wrap.appendChild(input);
|
|
635
|
+
form.appendChild(wrap);
|
|
636
|
+
}
|
|
637
|
+
const row = document.createElement('div');
|
|
638
|
+
row.className = 'row';
|
|
639
|
+
const ok = mkBtn('Submit');
|
|
640
|
+
ok.addEventListener('click', () => submit({ status: 'ok', values }));
|
|
641
|
+
const cancel = mkBtn('Cancel', true);
|
|
642
|
+
cancel.addEventListener('click', () => submit({ status: 'cancel' }));
|
|
643
|
+
row.appendChild(ok);
|
|
644
|
+
row.appendChild(cancel);
|
|
645
|
+
form.appendChild(row);
|
|
646
|
+
} else if (kind === 'choice') {
|
|
647
|
+
const options = Array.isArray(req?.prompt?.options) ? req.prompt.options : [];
|
|
648
|
+
const multiple = Boolean(req?.prompt?.multiple);
|
|
649
|
+
const selected = new Set();
|
|
650
|
+
const wrap = document.createElement('div');
|
|
651
|
+
const lab = document.createElement('label');
|
|
652
|
+
lab.textContent = '选择';
|
|
653
|
+
const select = document.createElement('select');
|
|
654
|
+
if (multiple) select.multiple = true;
|
|
655
|
+
for (const opt of options) {
|
|
656
|
+
const v = String(opt?.value || '');
|
|
657
|
+
const o = document.createElement('option');
|
|
658
|
+
o.value = v;
|
|
659
|
+
o.textContent = opt?.label ? String(opt.label) : v;
|
|
660
|
+
select.appendChild(o);
|
|
661
|
+
}
|
|
662
|
+
select.addEventListener('change', () => {
|
|
663
|
+
selected.clear();
|
|
664
|
+
for (const o of select.selectedOptions) selected.add(String(o.value));
|
|
665
|
+
});
|
|
666
|
+
wrap.appendChild(lab);
|
|
667
|
+
wrap.appendChild(select);
|
|
668
|
+
form.appendChild(wrap);
|
|
669
|
+
const row = document.createElement('div');
|
|
670
|
+
row.className = 'row';
|
|
671
|
+
const ok = mkBtn('Submit');
|
|
672
|
+
ok.addEventListener('click', () => submit({ status: 'ok', value: multiple ? Array.from(selected) : Array.from(selected)[0] || '' }));
|
|
673
|
+
const cancel = mkBtn('Cancel', true);
|
|
674
|
+
cancel.addEventListener('click', () => submit({ status: 'cancel' }));
|
|
675
|
+
row.appendChild(ok);
|
|
676
|
+
row.appendChild(cancel);
|
|
677
|
+
form.appendChild(row);
|
|
678
|
+
} else {
|
|
679
|
+
const row = document.createElement('div');
|
|
680
|
+
row.className = 'row';
|
|
681
|
+
const ok = mkBtn('OK');
|
|
682
|
+
ok.addEventListener('click', () => submit({ status: 'ok' }));
|
|
683
|
+
const cancel = mkBtn('Cancel', true);
|
|
684
|
+
cancel.addEventListener('click', () => submit({ status: 'cancel' }));
|
|
685
|
+
row.appendChild(ok);
|
|
686
|
+
row.appendChild(cancel);
|
|
687
|
+
form.appendChild(row);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
card.appendChild(title);
|
|
691
|
+
if (msg.textContent) card.appendChild(msg);
|
|
692
|
+
if (source.textContent) card.appendChild(source);
|
|
693
|
+
card.appendChild(form);
|
|
694
|
+
panelBody.appendChild(card);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const getTheme = () => currentTheme || resolveTheme();
|
|
699
|
+
|
|
700
|
+
const host = {
|
|
701
|
+
bridge: { enabled: true },
|
|
702
|
+
context: { get: () => ({ pluginId: __SANDBOX__.pluginId, appId: __SANDBOX__.appId, theme: getTheme(), bridge: { enabled: true } }) },
|
|
703
|
+
theme: {
|
|
704
|
+
get: getTheme,
|
|
705
|
+
onChange: (listener) => {
|
|
706
|
+
if (typeof listener !== 'function') return () => {};
|
|
707
|
+
themeListeners.add(listener);
|
|
708
|
+
return () => themeListeners.delete(listener);
|
|
709
|
+
},
|
|
710
|
+
},
|
|
711
|
+
admin: {
|
|
712
|
+
state: async () => ({ ok: true, state: {} }),
|
|
713
|
+
onUpdate: () => () => {},
|
|
714
|
+
models: { list: async () => ({ ok: true, models: [] }) },
|
|
715
|
+
secrets: { list: async () => ({ ok: true, secrets: [] }) },
|
|
716
|
+
},
|
|
717
|
+
registry: {
|
|
718
|
+
list: async () => ({ ok: true, apps: [__SANDBOX__.registryApp] }),
|
|
719
|
+
},
|
|
720
|
+
backend: {
|
|
721
|
+
invoke: async (method, params) => {
|
|
722
|
+
const r = await fetch('/api/backend/invoke', {
|
|
723
|
+
method: 'POST',
|
|
724
|
+
headers: { 'content-type': 'application/json' },
|
|
725
|
+
body: JSON.stringify({ method, params }),
|
|
726
|
+
});
|
|
727
|
+
const j = await r.json();
|
|
728
|
+
if (j?.ok === false) throw new Error(j?.message || 'invoke failed');
|
|
729
|
+
return j?.result;
|
|
730
|
+
},
|
|
731
|
+
},
|
|
732
|
+
uiPrompts: {
|
|
733
|
+
read: async () => ({ path: '(sandbox)', entries: [...entries] }),
|
|
734
|
+
onUpdate: (listener) => { listeners.add(listener); return () => listeners.delete(listener); },
|
|
735
|
+
request: async (payload) => {
|
|
736
|
+
const requestId = payload?.requestId ? String(payload.requestId) : uuid();
|
|
737
|
+
const prompt = payload?.prompt && typeof payload.prompt === 'object' ? { ...payload.prompt } : null;
|
|
738
|
+
if (prompt && !prompt.source) prompt.source = __SANDBOX__.pluginId + ':' + __SANDBOX__.appId;
|
|
739
|
+
entries.push({ ts: new Date().toISOString(), type: 'ui_prompt', action: 'request', requestId, runId: payload?.runId, prompt });
|
|
740
|
+
emitUpdate();
|
|
741
|
+
return { ok: true, requestId };
|
|
742
|
+
},
|
|
743
|
+
respond: async (payload) => {
|
|
744
|
+
const requestId = String(payload?.requestId || '');
|
|
745
|
+
if (!requestId) throw new Error('requestId is required');
|
|
746
|
+
const response = payload?.response && typeof payload.response === 'object' ? payload.response : null;
|
|
747
|
+
entries.push({ ts: new Date().toISOString(), type: 'ui_prompt', action: 'response', requestId, runId: payload?.runId, response });
|
|
748
|
+
emitUpdate();
|
|
749
|
+
return { ok: true };
|
|
750
|
+
},
|
|
751
|
+
open: () => (setPanelOpen(true), { ok: true }),
|
|
752
|
+
close: () => (setPanelOpen(false), { ok: true }),
|
|
753
|
+
toggle: () => (setPanelOpen(panel.style.display !== 'flex'), { ok: true }),
|
|
754
|
+
},
|
|
755
|
+
ui: { navigate: (menu) => ({ ok: true, menu }) },
|
|
756
|
+
chat: (() => {
|
|
757
|
+
const clone = (v) => JSON.parse(JSON.stringify(v));
|
|
758
|
+
|
|
759
|
+
const agents = [
|
|
760
|
+
{
|
|
761
|
+
id: 'sandbox-agent',
|
|
762
|
+
name: 'Sandbox Agent',
|
|
763
|
+
description: 'Mock agent for ChatOS UI Apps Sandbox',
|
|
764
|
+
},
|
|
765
|
+
];
|
|
766
|
+
|
|
767
|
+
const sessions = new Map();
|
|
768
|
+
const defaultSessionByAgent = new Map();
|
|
769
|
+
const messagesBySession = new Map();
|
|
770
|
+
|
|
771
|
+
const listeners = new Set();
|
|
772
|
+
const activeRuns = new Map(); // sessionId -> { aborted: boolean, timers: number[] }
|
|
773
|
+
|
|
774
|
+
const emit = (payload) => {
|
|
775
|
+
for (const sub of listeners) {
|
|
776
|
+
const filter = sub?.filter && typeof sub.filter === 'object' ? sub.filter : {};
|
|
777
|
+
if (filter?.sessionId && String(filter.sessionId) !== String(payload?.sessionId || '')) continue;
|
|
778
|
+
if (Array.isArray(filter?.types) && filter.types.length > 0) {
|
|
779
|
+
const t = String(payload?.type || '');
|
|
780
|
+
if (!filter.types.includes(t)) continue;
|
|
781
|
+
}
|
|
782
|
+
try {
|
|
783
|
+
sub.fn(payload);
|
|
784
|
+
} catch {
|
|
785
|
+
// ignore
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
const ensureAgent = async () => {
|
|
791
|
+
if (agents.length > 0) return agents[0];
|
|
792
|
+
const created = { id: 'sandbox-agent', name: 'Sandbox Agent', description: 'Mock agent' };
|
|
793
|
+
agents.push(created);
|
|
794
|
+
return created;
|
|
795
|
+
};
|
|
796
|
+
|
|
797
|
+
const ensureSession = async (agentId) => {
|
|
798
|
+
const aid = String(agentId || '').trim() || (await ensureAgent()).id;
|
|
799
|
+
const existingId = defaultSessionByAgent.get(aid);
|
|
800
|
+
if (existingId && sessions.has(existingId)) return sessions.get(existingId);
|
|
801
|
+
|
|
802
|
+
const id = 'sandbox-session-' + uuid();
|
|
803
|
+
const session = { id, agentId: aid, createdAt: new Date().toISOString() };
|
|
804
|
+
sessions.set(id, session);
|
|
805
|
+
defaultSessionByAgent.set(aid, id);
|
|
806
|
+
if (!messagesBySession.has(id)) messagesBySession.set(id, []);
|
|
807
|
+
return session;
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
const agentsApi = {
|
|
811
|
+
list: async () => ({ ok: true, agents: clone(agents) }),
|
|
812
|
+
ensureDefault: async () => ({ ok: true, agent: clone(await ensureAgent()) }),
|
|
813
|
+
create: async (payload) => {
|
|
814
|
+
const agent = {
|
|
815
|
+
id: 'sandbox-agent-' + uuid(),
|
|
816
|
+
name: payload?.name ? String(payload.name) : 'Sandbox Agent',
|
|
817
|
+
description: payload?.description ? String(payload.description) : '',
|
|
818
|
+
};
|
|
819
|
+
agents.unshift(agent);
|
|
820
|
+
return { ok: true, agent: clone(agent) };
|
|
821
|
+
},
|
|
822
|
+
update: async (id, patch) => {
|
|
823
|
+
const agentId = String(id || '').trim();
|
|
824
|
+
if (!agentId) throw new Error('id is required');
|
|
825
|
+
const idx = agents.findIndex((a) => a.id === agentId);
|
|
826
|
+
if (idx < 0) throw new Error('agent not found');
|
|
827
|
+
const a = agents[idx];
|
|
828
|
+
if (patch?.name) a.name = String(patch.name);
|
|
829
|
+
if (patch?.description) a.description = String(patch.description);
|
|
830
|
+
return { ok: true, agent: clone(a) };
|
|
831
|
+
},
|
|
832
|
+
delete: async (id) => {
|
|
833
|
+
const agentId = String(id || '').trim();
|
|
834
|
+
if (!agentId) throw new Error('id is required');
|
|
835
|
+
const idx = agents.findIndex((a) => a.id === agentId);
|
|
836
|
+
if (idx < 0) return { ok: true, deleted: false };
|
|
837
|
+
agents.splice(idx, 1);
|
|
838
|
+
return { ok: true, deleted: true };
|
|
839
|
+
},
|
|
840
|
+
createForApp: async (payload) => {
|
|
841
|
+
const name = payload?.name ? String(payload.name) : 'App Agent (' + __SANDBOX__.appId + ')';
|
|
842
|
+
return await agentsApi.create({ ...payload, name });
|
|
843
|
+
},
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
const sessionsApi = {
|
|
847
|
+
list: async () => ({ ok: true, sessions: clone(Array.from(sessions.values())) }),
|
|
848
|
+
ensureDefault: async (payload) => {
|
|
849
|
+
const session = await ensureSession(payload?.agentId);
|
|
850
|
+
return { ok: true, session: clone(session) };
|
|
851
|
+
},
|
|
852
|
+
create: async (payload) => {
|
|
853
|
+
const agentId = payload?.agentId ? String(payload.agentId) : (await ensureAgent()).id;
|
|
854
|
+
const id = 'sandbox-session-' + uuid();
|
|
855
|
+
const session = { id, agentId, createdAt: new Date().toISOString() };
|
|
856
|
+
sessions.set(id, session);
|
|
857
|
+
if (!messagesBySession.has(id)) messagesBySession.set(id, []);
|
|
858
|
+
return { ok: true, session: clone(session) };
|
|
859
|
+
},
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
const messagesApi = {
|
|
863
|
+
list: async (payload) => {
|
|
864
|
+
const sessionId = String(payload?.sessionId || '').trim();
|
|
865
|
+
if (!sessionId) throw new Error('sessionId is required');
|
|
866
|
+
const msgs = messagesBySession.get(sessionId) || [];
|
|
867
|
+
return { ok: true, messages: clone(msgs) };
|
|
868
|
+
},
|
|
869
|
+
};
|
|
870
|
+
|
|
871
|
+
const abort = async (payload) => {
|
|
872
|
+
const sessionId = String(payload?.sessionId || '').trim();
|
|
873
|
+
if (!sessionId) throw new Error('sessionId is required');
|
|
874
|
+
const run = activeRuns.get(sessionId);
|
|
875
|
+
if (run) {
|
|
876
|
+
run.aborted = true;
|
|
877
|
+
for (const t of run.timers) {
|
|
878
|
+
try {
|
|
879
|
+
clearTimeout(t);
|
|
880
|
+
} catch {
|
|
881
|
+
// ignore
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
activeRuns.delete(sessionId);
|
|
885
|
+
}
|
|
886
|
+
emit({ type: 'assistant_abort', sessionId, ts: new Date().toISOString() });
|
|
887
|
+
return { ok: true };
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
const send = async (payload) => {
|
|
891
|
+
const sessionId = String(payload?.sessionId || '').trim();
|
|
892
|
+
const text = String(payload?.text || '').trim();
|
|
893
|
+
if (!sessionId) throw new Error('sessionId is required');
|
|
894
|
+
if (!text) throw new Error('text is required');
|
|
895
|
+
|
|
896
|
+
if (!sessions.has(sessionId)) throw new Error('session not found');
|
|
897
|
+
|
|
898
|
+
const msgs = messagesBySession.get(sessionId) || [];
|
|
899
|
+
const userMsg = { id: 'msg-' + uuid(), role: 'user', text, ts: new Date().toISOString() };
|
|
900
|
+
msgs.push(userMsg);
|
|
901
|
+
messagesBySession.set(sessionId, msgs);
|
|
902
|
+
emit({ type: 'user_message', sessionId, message: clone(userMsg) });
|
|
903
|
+
|
|
904
|
+
const assistantMsg = { id: 'msg-' + uuid(), role: 'assistant', text: '', ts: new Date().toISOString() };
|
|
905
|
+
msgs.push(assistantMsg);
|
|
906
|
+
emit({ type: 'assistant_start', sessionId, message: clone(assistantMsg) });
|
|
907
|
+
|
|
908
|
+
const out = '[sandbox] echo: ' + text;
|
|
909
|
+
const chunks = [];
|
|
910
|
+
for (let i = 0; i < out.length; i += 8) chunks.push(out.slice(i, i + 8));
|
|
911
|
+
|
|
912
|
+
const run = { aborted: false, timers: [] };
|
|
913
|
+
activeRuns.set(sessionId, run);
|
|
914
|
+
|
|
915
|
+
chunks.forEach((delta, idx) => {
|
|
916
|
+
const t = setTimeout(() => {
|
|
917
|
+
if (run.aborted) return;
|
|
918
|
+
assistantMsg.text += delta;
|
|
919
|
+
emit({ type: 'assistant_delta', sessionId, delta });
|
|
920
|
+
if (idx === chunks.length - 1) {
|
|
921
|
+
activeRuns.delete(sessionId);
|
|
922
|
+
emit({ type: 'assistant_end', sessionId, message: clone(assistantMsg) });
|
|
923
|
+
}
|
|
924
|
+
}, 80 + idx * 60);
|
|
925
|
+
run.timers.push(t);
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
return { ok: true };
|
|
929
|
+
};
|
|
930
|
+
|
|
931
|
+
const events = {
|
|
932
|
+
subscribe: (filter, fn) => {
|
|
933
|
+
if (typeof fn !== 'function') throw new Error('listener is required');
|
|
934
|
+
const sub = { filter: filter && typeof filter === 'object' ? { ...filter } : {}, fn };
|
|
935
|
+
listeners.add(sub);
|
|
936
|
+
return () => listeners.delete(sub);
|
|
937
|
+
},
|
|
938
|
+
unsubscribe: () => (listeners.clear(), { ok: true }),
|
|
939
|
+
};
|
|
940
|
+
|
|
941
|
+
return {
|
|
942
|
+
agents: agentsApi,
|
|
943
|
+
sessions: sessionsApi,
|
|
944
|
+
messages: messagesApi,
|
|
945
|
+
send,
|
|
946
|
+
abort,
|
|
947
|
+
events,
|
|
948
|
+
};
|
|
949
|
+
})(),
|
|
950
|
+
};
|
|
951
|
+
|
|
952
|
+
inspectorEnabled = true;
|
|
953
|
+
updateInspector();
|
|
954
|
+
|
|
955
|
+
let dispose = null;
|
|
956
|
+
|
|
957
|
+
async function loadAndMount() {
|
|
958
|
+
if (typeof dispose === 'function') { try { await dispose(); } catch {} dispose = null; }
|
|
959
|
+
container.textContent = '';
|
|
960
|
+
|
|
961
|
+
const entryUrl = __SANDBOX__.entryUrl;
|
|
962
|
+
const mod = await import(entryUrl + (entryUrl.includes('?') ? '&' : '?') + 't=' + Date.now());
|
|
963
|
+
const mount = mod?.mount || mod?.default?.mount || (typeof mod?.default === 'function' ? mod.default : null);
|
|
964
|
+
if (typeof mount !== 'function') throw new Error('module entry must export mount()');
|
|
965
|
+
const ret = await mount({ container, host, slots: { header: headerSlot } });
|
|
966
|
+
if (typeof ret === 'function') dispose = ret;
|
|
967
|
+
else if (ret && typeof ret.dispose === 'function') dispose = () => ret.dispose();
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
const renderError = (e) => {
|
|
971
|
+
const pre = document.createElement('pre');
|
|
972
|
+
pre.style.padding = '12px';
|
|
973
|
+
pre.style.whiteSpace = 'pre-wrap';
|
|
974
|
+
pre.textContent = '[sandbox] ' + (e?.stack || e?.message || String(e));
|
|
975
|
+
container.appendChild(pre);
|
|
976
|
+
};
|
|
977
|
+
|
|
978
|
+
const scheduleReload = (() => {
|
|
979
|
+
let t = null;
|
|
980
|
+
return () => {
|
|
981
|
+
if (t) return;
|
|
982
|
+
t = setTimeout(() => {
|
|
983
|
+
t = null;
|
|
984
|
+
loadAndMount().catch(renderError);
|
|
985
|
+
}, 80);
|
|
986
|
+
};
|
|
987
|
+
})();
|
|
988
|
+
|
|
989
|
+
try {
|
|
990
|
+
const es = new EventSource('/events');
|
|
991
|
+
es.addEventListener('reload', () => scheduleReload());
|
|
992
|
+
} catch {
|
|
993
|
+
// ignore
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
$('#btnReload').addEventListener('click', () => loadAndMount().catch(renderError));
|
|
997
|
+
|
|
998
|
+
loadAndMount().catch(renderError);
|
|
999
|
+
`;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
async function loadBackendFactory({ pluginDir, manifest }) {
|
|
1003
|
+
const entryRel = manifest?.backend?.entry ? String(manifest.backend.entry).trim() : '';
|
|
1004
|
+
if (!entryRel) return null;
|
|
1005
|
+
const abs = resolveInsideDir(pluginDir, entryRel);
|
|
1006
|
+
const fileUrl = url.pathToFileURL(abs).toString();
|
|
1007
|
+
const mod = await import(fileUrl + `?t=${Date.now()}`);
|
|
1008
|
+
if (typeof mod?.createUiAppsBackend !== 'function') {
|
|
1009
|
+
throw new Error('backend entry must export createUiAppsBackend(ctx)');
|
|
1010
|
+
}
|
|
1011
|
+
return mod.createUiAppsBackend;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
export async function startSandboxServer({ pluginDir, port = 4399, appId = '' }) {
|
|
1015
|
+
const { manifest } = loadPluginManifest(pluginDir);
|
|
1016
|
+
const app = pickAppFromManifest(manifest, appId);
|
|
1017
|
+
const effectiveAppId = String(app?.id || '');
|
|
1018
|
+
const entryRel = String(app?.entry?.path || '').trim();
|
|
1019
|
+
if (!entryRel) throw new Error('apps[i].entry.path is required');
|
|
1020
|
+
|
|
1021
|
+
const entryAbs = resolveInsideDir(pluginDir, entryRel);
|
|
1022
|
+
if (!isFile(entryAbs)) throw new Error(`module entry not found: ${entryRel}`);
|
|
1023
|
+
|
|
1024
|
+
const entryUrl = `/plugin/${encodeURIComponent(entryRel).replaceAll('%2F', '/')}`;
|
|
1025
|
+
|
|
1026
|
+
let backendInstance = null;
|
|
1027
|
+
let backendFactory = null;
|
|
1028
|
+
|
|
1029
|
+
const ctxBase = {
|
|
1030
|
+
pluginId: String(manifest?.id || ''),
|
|
1031
|
+
pluginDir,
|
|
1032
|
+
stateDir: path.join(process.cwd(), '.chatos', 'state', 'chatos'),
|
|
1033
|
+
sessionRoot: process.cwd(),
|
|
1034
|
+
projectRoot: process.cwd(),
|
|
1035
|
+
dataDir: '',
|
|
1036
|
+
llm: {
|
|
1037
|
+
complete: async (payload) => {
|
|
1038
|
+
const input = typeof payload?.input === 'string' ? payload.input : '';
|
|
1039
|
+
const normalized = String(input || '').trim();
|
|
1040
|
+
if (!normalized) throw new Error('input is required');
|
|
1041
|
+
const modelName =
|
|
1042
|
+
typeof payload?.modelName === 'string' && payload.modelName.trim()
|
|
1043
|
+
? payload.modelName.trim()
|
|
1044
|
+
: typeof payload?.modelId === 'string' && payload.modelId.trim()
|
|
1045
|
+
? `model:${payload.modelId.trim()}`
|
|
1046
|
+
: 'sandbox';
|
|
1047
|
+
return {
|
|
1048
|
+
ok: true,
|
|
1049
|
+
model: modelName,
|
|
1050
|
+
content: `[sandbox llm] ${normalized}`,
|
|
1051
|
+
};
|
|
1052
|
+
},
|
|
1053
|
+
},
|
|
1054
|
+
};
|
|
1055
|
+
ctxBase.dataDir = path.join(process.cwd(), '.chatos', 'data', ctxBase.pluginId);
|
|
1056
|
+
ensureDir(ctxBase.stateDir);
|
|
1057
|
+
ensureDir(ctxBase.dataDir);
|
|
1058
|
+
|
|
1059
|
+
const sseClients = new Set();
|
|
1060
|
+
const sseWrite = (res, event, data) => {
|
|
1061
|
+
try {
|
|
1062
|
+
res.write(`event: ${event}\n`);
|
|
1063
|
+
res.write(`data: ${JSON.stringify(data ?? null)}\n\n`);
|
|
1064
|
+
} catch {
|
|
1065
|
+
// ignore
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
const sseBroadcast = (event, data) => {
|
|
1069
|
+
for (const res of sseClients) {
|
|
1070
|
+
sseWrite(res, event, data);
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
|
|
1074
|
+
let changeSeq = 0;
|
|
1075
|
+
const stopWatch = startRecursiveWatcher(pluginDir, ({ eventType, filePath }) => {
|
|
1076
|
+
const rel = filePath ? path.relative(pluginDir, filePath).replaceAll('\\', '/') : '';
|
|
1077
|
+
const base = rel ? path.basename(rel) : '';
|
|
1078
|
+
if (!rel) return;
|
|
1079
|
+
if (base === '.DS_Store') return;
|
|
1080
|
+
if (base.endsWith('.map')) return;
|
|
1081
|
+
|
|
1082
|
+
changeSeq += 1;
|
|
1083
|
+
if (rel.startsWith('backend/')) {
|
|
1084
|
+
backendInstance = null;
|
|
1085
|
+
backendFactory = null;
|
|
1086
|
+
}
|
|
1087
|
+
sseBroadcast('reload', { seq: changeSeq, eventType: eventType || '', path: rel });
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
const server = http.createServer(async (req, res) => {
|
|
1091
|
+
try {
|
|
1092
|
+
const parsed = url.parse(req.url || '/', true);
|
|
1093
|
+
const pathname = parsed.pathname || '/';
|
|
1094
|
+
|
|
1095
|
+
if (req.method === 'GET' && pathname === '/') {
|
|
1096
|
+
return sendText(res, 200, htmlPage(), 'text/html; charset=utf-8');
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
if (req.method === 'GET' && pathname === '/events') {
|
|
1100
|
+
res.writeHead(200, {
|
|
1101
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
1102
|
+
'cache-control': 'no-store',
|
|
1103
|
+
connection: 'keep-alive',
|
|
1104
|
+
});
|
|
1105
|
+
res.write(': connected\n\n');
|
|
1106
|
+
sseClients.add(res);
|
|
1107
|
+
const ping = setInterval(() => {
|
|
1108
|
+
try {
|
|
1109
|
+
res.write(': ping\n\n');
|
|
1110
|
+
} catch {
|
|
1111
|
+
// ignore
|
|
1112
|
+
}
|
|
1113
|
+
}, 15000);
|
|
1114
|
+
req.on('close', () => {
|
|
1115
|
+
try {
|
|
1116
|
+
clearInterval(ping);
|
|
1117
|
+
} catch {
|
|
1118
|
+
// ignore
|
|
1119
|
+
}
|
|
1120
|
+
sseClients.delete(res);
|
|
1121
|
+
});
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
if (req.method === 'GET' && pathname === '/sandbox.mjs') {
|
|
1126
|
+
const tokenNames = loadTokenNames();
|
|
1127
|
+
const js = sandboxClientJs()
|
|
1128
|
+
.replaceAll('__SANDBOX__.pluginId', JSON.stringify(ctxBase.pluginId))
|
|
1129
|
+
.replaceAll('__SANDBOX__.appId', JSON.stringify(effectiveAppId))
|
|
1130
|
+
.replaceAll('__SANDBOX__.entryUrl', JSON.stringify(entryUrl))
|
|
1131
|
+
.replaceAll('__SANDBOX__.registryApp', JSON.stringify({ plugin: { id: ctxBase.pluginId }, id: effectiveAppId, entry: { type: 'module', url: entryUrl } }))
|
|
1132
|
+
.replaceAll('__SANDBOX__.tokenNames', JSON.stringify(tokenNames));
|
|
1133
|
+
return sendText(res, 200, js, 'text/javascript; charset=utf-8');
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
if (req.method === 'GET' && pathname.startsWith('/plugin/')) {
|
|
1137
|
+
const rel = decodeURIComponent(pathname.slice('/plugin/'.length));
|
|
1138
|
+
const abs = resolveInsideDir(pluginDir, rel);
|
|
1139
|
+
if (!serveStaticFile(res, abs)) return sendText(res, 404, 'Not found');
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
if (req.method === 'GET' && pathname === '/api/manifest') {
|
|
1144
|
+
return sendJson(res, 200, { ok: true, manifest });
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
if (pathname === '/api/backend/invoke') {
|
|
1148
|
+
if (req.method !== 'POST') return sendJson(res, 405, { ok: false, message: 'Method not allowed' });
|
|
1149
|
+
let body = '';
|
|
1150
|
+
req.on('data', (chunk) => {
|
|
1151
|
+
body += chunk;
|
|
1152
|
+
});
|
|
1153
|
+
req.on('end', async () => {
|
|
1154
|
+
try {
|
|
1155
|
+
const payload = body ? JSON.parse(body) : {};
|
|
1156
|
+
const method = typeof payload?.method === 'string' ? payload.method.trim() : '';
|
|
1157
|
+
if (!method) return sendJson(res, 400, { ok: false, message: 'method is required' });
|
|
1158
|
+
const params = payload?.params;
|
|
1159
|
+
|
|
1160
|
+
if (!backendFactory) backendFactory = await loadBackendFactory({ pluginDir, manifest });
|
|
1161
|
+
if (!backendFactory) return sendJson(res, 200, { ok: false, message: 'backend not configured in plugin.json' });
|
|
1162
|
+
|
|
1163
|
+
if (!backendInstance || typeof backendInstance !== 'object' || !backendInstance.methods) {
|
|
1164
|
+
backendInstance = await backendFactory({ ...ctxBase });
|
|
1165
|
+
}
|
|
1166
|
+
const fn = backendInstance?.methods?.[method];
|
|
1167
|
+
if (typeof fn !== 'function') return sendJson(res, 404, { ok: false, message: `method not found: ${method}` });
|
|
1168
|
+
const result = await fn(params, { ...ctxBase });
|
|
1169
|
+
return sendJson(res, 200, { ok: true, result });
|
|
1170
|
+
} catch (e) {
|
|
1171
|
+
return sendJson(res, 200, { ok: false, message: e?.message || String(e) });
|
|
1172
|
+
}
|
|
1173
|
+
});
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
sendText(res, 404, 'Not found');
|
|
1178
|
+
} catch (e) {
|
|
1179
|
+
sendJson(res, 500, { ok: false, message: e?.message || String(e) });
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
server.once('close', () => stopWatch());
|
|
1183
|
+
|
|
1184
|
+
await new Promise((resolve, reject) => {
|
|
1185
|
+
server.once('error', reject);
|
|
1186
|
+
server.listen(port, '127.0.0.1', () => {
|
|
1187
|
+
server.off('error', reject);
|
|
1188
|
+
resolve();
|
|
1189
|
+
});
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
// eslint-disable-next-line no-console
|
|
1193
|
+
console.log(`Sandbox running:
|
|
1194
|
+
http://localhost:${port}/
|
|
1195
|
+
pluginDir:
|
|
1196
|
+
${pluginDir}
|
|
1197
|
+
app:
|
|
1198
|
+
${ctxBase.pluginId}:${effectiveAppId}
|
|
1199
|
+
`);
|
|
1200
|
+
}
|