@leeoohoo/ui-apps-devkit 0.1.2 → 0.1.4
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 +61 -62
- package/bin/chatos-uiapp.js +3 -4
- package/package.json +23 -23
- package/src/cli.js +53 -53
- package/src/commands/dev.js +14 -14
- package/src/commands/init.js +129 -129
- package/src/commands/install.js +45 -45
- package/src/commands/pack.js +72 -72
- package/src/commands/validate.js +90 -138
- package/src/lib/args.js +63 -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 -172
- package/src/sandbox/server.js +1204 -1028
- package/templates/basic/README.md +63 -65
- package/templates/basic/chatos.config.json +5 -5
- package/templates/basic/docs/CHATOS_UI_APPS_AI_CONTRIBUTIONS.md +209 -211
- package/templates/basic/docs/CHATOS_UI_APPS_BACKEND_PROTOCOL.md +73 -73
- package/templates/basic/docs/CHATOS_UI_APPS_HOST_API.md +136 -136
- package/templates/basic/docs/CHATOS_UI_APPS_OVERVIEW.md +106 -106
- package/templates/basic/docs/CHATOS_UI_APPS_PLUGIN_MANIFEST.md +239 -239
- package/templates/basic/docs/CHATOS_UI_APPS_STYLE_GUIDE.md +95 -95
- package/templates/basic/docs/CHATOS_UI_APPS_TROUBLESHOOTING.md +40 -40
- package/templates/basic/docs/CHATOS_UI_PROMPTS_PROTOCOL.md +392 -392
- package/templates/basic/plugin/apps/app/compact.mjs +41 -41
- package/templates/basic/plugin/apps/app/index.mjs +287 -287
- 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 +38 -44
- package/templates/notepad/chatos.config.json +4 -4
- package/templates/notepad/docs/CHATOS_UI_APPS_AI_CONTRIBUTIONS.md +209 -211
- package/templates/notepad/docs/CHATOS_UI_APPS_BACKEND_PROTOCOL.md +73 -73
- package/templates/notepad/docs/CHATOS_UI_APPS_HOST_API.md +136 -136
- package/templates/notepad/docs/CHATOS_UI_APPS_OVERVIEW.md +106 -106
- package/templates/notepad/docs/CHATOS_UI_APPS_PLUGIN_MANIFEST.md +239 -239
- package/templates/notepad/docs/CHATOS_UI_APPS_STYLE_GUIDE.md +95 -95
- package/templates/notepad/docs/CHATOS_UI_APPS_TROUBLESHOOTING.md +40 -40
- 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 -41
- 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 +199 -199
- 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 +39 -39
- package/templates/notepad/plugin/shared/notepad-store.mjs +765 -765
- package/templates/notepad/template.json +8 -8
package/src/sandbox/server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
1
|
+
import fs from 'fs';
|
|
2
2
|
import http from 'http';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import url from 'url';
|
|
@@ -13,29 +13,29 @@ import { copyDir, ensureDir, isDirectory, isFile } from '../lib/fs.js';
|
|
|
13
13
|
import { loadPluginManifest, pickAppFromManifest } from '../lib/plugin.js';
|
|
14
14
|
import { resolveInsideDir } from '../lib/path-boundary.js';
|
|
15
15
|
import { COMPAT_STATE_ROOT_DIRNAME, STATE_ROOT_DIRNAME } from '../lib/state-constants.js';
|
|
16
|
-
|
|
17
|
-
const __filename = url.fileURLToPath(import.meta.url);
|
|
16
|
+
|
|
17
|
+
const __filename = url.fileURLToPath(import.meta.url);
|
|
18
18
|
const __dirname = path.dirname(__filename);
|
|
19
19
|
|
|
20
20
|
const TOKEN_REGEX = /--ds-[a-z0-9-]+/gi;
|
|
21
21
|
const SANDBOX_STATE_DIRNAME = STATE_ROOT_DIRNAME;
|
|
22
22
|
const SANDBOX_COMPAT_DIRNAME = COMPAT_STATE_ROOT_DIRNAME;
|
|
23
|
-
const GLOBAL_STYLES_CANDIDATES = [
|
|
24
|
-
path.resolve(__dirname, '..', '..', '..', 'common', 'aide-ui', 'components', 'GlobalStyles.jsx'),
|
|
25
|
-
path.resolve(process.cwd(), 'common', 'aide-ui', 'components', 'GlobalStyles.jsx'),
|
|
26
|
-
];
|
|
27
|
-
|
|
23
|
+
const GLOBAL_STYLES_CANDIDATES = [
|
|
24
|
+
path.resolve(__dirname, '..', '..', '..', 'common', 'aide-ui', 'components', 'GlobalStyles.jsx'),
|
|
25
|
+
path.resolve(process.cwd(), 'common', 'aide-ui', 'components', 'GlobalStyles.jsx'),
|
|
26
|
+
];
|
|
27
|
+
|
|
28
28
|
function loadTokenNames() {
|
|
29
29
|
for (const candidate of GLOBAL_STYLES_CANDIDATES) {
|
|
30
30
|
try {
|
|
31
31
|
if (!isFile(candidate)) continue;
|
|
32
32
|
const raw = fs.readFileSync(candidate, 'utf8');
|
|
33
|
-
const matches = raw.match(TOKEN_REGEX) || [];
|
|
34
|
-
const names = Array.from(new Set(matches.map((v) => v.toLowerCase())));
|
|
35
|
-
if (names.length > 0) return names.sort();
|
|
36
|
-
} catch {
|
|
37
|
-
// ignore
|
|
38
|
-
}
|
|
33
|
+
const matches = raw.match(TOKEN_REGEX) || [];
|
|
34
|
+
const names = Array.from(new Set(matches.map((v) => v.toLowerCase())));
|
|
35
|
+
if (names.length > 0) return names.sort();
|
|
36
|
+
} catch {
|
|
37
|
+
// ignore
|
|
38
|
+
}
|
|
39
39
|
}
|
|
40
40
|
return [];
|
|
41
41
|
}
|
|
@@ -126,7 +126,7 @@ function expandCallMetaValue(value, vars) {
|
|
|
126
126
|
return value;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
function buildSandboxCallMeta({ rawCallMeta,
|
|
129
|
+
function buildSandboxCallMeta({ rawCallMeta, context } = {}) {
|
|
130
130
|
const ctx = context && typeof context === 'object' ? context : null;
|
|
131
131
|
const defaults = ctx
|
|
132
132
|
? {
|
|
@@ -158,32 +158,22 @@ function buildSandboxCallMeta({ rawCallMeta, rawWorkdir, context } = {}) {
|
|
|
158
158
|
}
|
|
159
159
|
: {};
|
|
160
160
|
const expanded = raw ? expandCallMetaValue(raw, vars) : null;
|
|
161
|
-
|
|
162
|
-
const workdirRaw = normalizeText(rawWorkdir);
|
|
163
|
-
if (workdirRaw) {
|
|
164
|
-
const expandedWorkdir = expandCallMetaValue(workdirRaw, vars);
|
|
165
|
-
const workdirValue = typeof expandedWorkdir === 'string' ? expandedWorkdir.trim() : '';
|
|
166
|
-
if (workdirValue) {
|
|
167
|
-
merged = mergeCallMeta(merged, { workdir: workdirValue });
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
return merged;
|
|
161
|
+
return mergeCallMeta(defaults, expanded);
|
|
171
162
|
}
|
|
172
163
|
|
|
173
164
|
function loadSandboxLlmConfig(filePath) {
|
|
174
|
-
if (!filePath) return { apiKey: '', baseUrl: '', modelId: ''
|
|
165
|
+
if (!filePath) return { apiKey: '', baseUrl: '', modelId: '' };
|
|
175
166
|
try {
|
|
176
|
-
if (!fs.existsSync(filePath)) return { apiKey: '', baseUrl: '', modelId: ''
|
|
167
|
+
if (!fs.existsSync(filePath)) return { apiKey: '', baseUrl: '', modelId: '' };
|
|
177
168
|
const raw = fs.readFileSync(filePath, 'utf8');
|
|
178
169
|
const parsed = raw ? JSON.parse(raw) : {};
|
|
179
170
|
return {
|
|
180
171
|
apiKey: normalizeText(parsed?.apiKey),
|
|
181
172
|
baseUrl: normalizeText(parsed?.baseUrl),
|
|
182
173
|
modelId: normalizeText(parsed?.modelId),
|
|
183
|
-
workdir: normalizeText(parsed?.workdir),
|
|
184
174
|
};
|
|
185
175
|
} catch {
|
|
186
|
-
return { apiKey: '', baseUrl: '', modelId: ''
|
|
176
|
+
return { apiKey: '', baseUrl: '', modelId: '' };
|
|
187
177
|
}
|
|
188
178
|
}
|
|
189
179
|
|
|
@@ -442,15 +432,15 @@ async function callOpenAiChat({ apiKey, baseUrl, model, messages, tools, signal
|
|
|
442
432
|
return await res.json();
|
|
443
433
|
}
|
|
444
434
|
|
|
445
|
-
function sendJson(res, status, obj) {
|
|
446
|
-
const raw = JSON.stringify(obj);
|
|
447
|
-
res.writeHead(status, {
|
|
448
|
-
'content-type': 'application/json; charset=utf-8',
|
|
449
|
-
'cache-control': 'no-store',
|
|
450
|
-
});
|
|
451
|
-
res.end(raw);
|
|
452
|
-
}
|
|
453
|
-
|
|
435
|
+
function sendJson(res, status, obj) {
|
|
436
|
+
const raw = JSON.stringify(obj);
|
|
437
|
+
res.writeHead(status, {
|
|
438
|
+
'content-type': 'application/json; charset=utf-8',
|
|
439
|
+
'cache-control': 'no-store',
|
|
440
|
+
});
|
|
441
|
+
res.end(raw);
|
|
442
|
+
}
|
|
443
|
+
|
|
454
444
|
function sendText(res, status, text, contentType) {
|
|
455
445
|
res.writeHead(status, {
|
|
456
446
|
'content-type': contentType || 'text/plain; charset=utf-8',
|
|
@@ -478,226 +468,226 @@ function readJsonBody(req) {
|
|
|
478
468
|
|
|
479
469
|
function guessContentType(filePath) {
|
|
480
470
|
const ext = path.extname(filePath).toLowerCase();
|
|
481
|
-
if (ext === '.html') return 'text/html; charset=utf-8';
|
|
482
|
-
if (ext === '.css') return 'text/css; charset=utf-8';
|
|
483
|
-
if (ext === '.mjs' || ext === '.js') return 'text/javascript; charset=utf-8';
|
|
484
|
-
if (ext === '.json') return 'application/json; charset=utf-8';
|
|
485
|
-
if (ext === '.md') return 'text/markdown; charset=utf-8';
|
|
486
|
-
if (ext === '.svg') return 'image/svg+xml';
|
|
487
|
-
if (ext === '.png') return 'image/png';
|
|
488
|
-
return 'application/octet-stream';
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
function serveStaticFile(res, filePath) {
|
|
492
|
-
if (!isFile(filePath)) return false;
|
|
493
|
-
const ct = guessContentType(filePath);
|
|
494
|
-
const buf = fs.readFileSync(filePath);
|
|
495
|
-
res.writeHead(200, { 'content-type': ct, 'cache-control': 'no-store' });
|
|
496
|
-
res.end(buf);
|
|
497
|
-
return true;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
function startRecursiveWatcher(rootDir, onChange) {
|
|
501
|
-
const root = path.resolve(rootDir);
|
|
502
|
-
if (!isDirectory(root)) return () => {};
|
|
503
|
-
|
|
504
|
-
const watchers = new Map();
|
|
505
|
-
|
|
506
|
-
const shouldIgnore = (p) => {
|
|
507
|
-
const base = path.basename(p);
|
|
508
|
-
if (!base) return false;
|
|
509
|
-
if (base === 'node_modules') return true;
|
|
510
|
-
if (base === '.git') return true;
|
|
511
|
-
if (base === '.DS_Store') return true;
|
|
512
|
-
return false;
|
|
513
|
-
};
|
|
514
|
-
|
|
515
|
-
const scan = (dir) => {
|
|
516
|
-
const abs = path.resolve(dir);
|
|
517
|
-
if (!isDirectory(abs)) return;
|
|
518
|
-
if (shouldIgnore(abs)) return;
|
|
519
|
-
if (!watchers.has(abs)) {
|
|
520
|
-
try {
|
|
521
|
-
const w = fs.watch(abs, (eventType, filename) => {
|
|
522
|
-
const relName = filename ? String(filename) : '';
|
|
523
|
-
const filePath = relName ? path.join(abs, relName) : abs;
|
|
524
|
-
try {
|
|
525
|
-
onChange({ eventType, filePath });
|
|
526
|
-
} catch {
|
|
527
|
-
// ignore
|
|
528
|
-
}
|
|
529
|
-
scheduleRescan();
|
|
530
|
-
});
|
|
531
|
-
watchers.set(abs, w);
|
|
532
|
-
} catch {
|
|
533
|
-
// ignore
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
let entries = [];
|
|
538
|
-
try {
|
|
539
|
-
entries = fs.readdirSync(abs, { withFileTypes: true });
|
|
540
|
-
} catch {
|
|
541
|
-
return;
|
|
542
|
-
}
|
|
543
|
-
for (const ent of entries) {
|
|
544
|
-
if (!ent?.isDirectory?.()) continue;
|
|
545
|
-
const child = path.join(abs, ent.name);
|
|
546
|
-
if (shouldIgnore(child)) continue;
|
|
547
|
-
scan(child);
|
|
548
|
-
}
|
|
549
|
-
};
|
|
550
|
-
|
|
551
|
-
let rescanTimer = null;
|
|
552
|
-
const scheduleRescan = () => {
|
|
553
|
-
if (rescanTimer) return;
|
|
554
|
-
rescanTimer = setTimeout(() => {
|
|
555
|
-
rescanTimer = null;
|
|
556
|
-
scan(root);
|
|
557
|
-
}, 250);
|
|
558
|
-
};
|
|
559
|
-
|
|
560
|
-
scan(root);
|
|
561
|
-
|
|
562
|
-
return () => {
|
|
563
|
-
if (rescanTimer) {
|
|
564
|
-
try {
|
|
565
|
-
clearTimeout(rescanTimer);
|
|
566
|
-
} catch {
|
|
567
|
-
// ignore
|
|
568
|
-
}
|
|
569
|
-
rescanTimer = null;
|
|
570
|
-
}
|
|
571
|
-
for (const w of watchers.values()) {
|
|
572
|
-
try {
|
|
573
|
-
w.close();
|
|
574
|
-
} catch {
|
|
575
|
-
// ignore
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
watchers.clear();
|
|
579
|
-
};
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
function htmlPage() {
|
|
583
|
-
return `<!doctype html>
|
|
584
|
-
<html lang="zh-CN">
|
|
585
|
-
<head>
|
|
586
|
-
<meta charset="UTF-8" />
|
|
587
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
588
|
-
<title>ChatOS UI Apps Sandbox</title>
|
|
589
|
-
<style>
|
|
590
|
-
:root {
|
|
591
|
-
color-scheme: light;
|
|
592
|
-
--ds-accent: #00d4ff;
|
|
593
|
-
--ds-accent-2: #7c3aed;
|
|
594
|
-
--ds-panel-bg: rgba(255, 255, 255, 0.86);
|
|
595
|
-
--ds-panel-border: rgba(15, 23, 42, 0.08);
|
|
596
|
-
--ds-subtle-bg: rgba(255, 255, 255, 0.62);
|
|
597
|
-
--ds-selected-bg: linear-gradient(90deg, rgba(0, 212, 255, 0.14), rgba(124, 58, 237, 0.08));
|
|
598
|
-
--ds-focus-ring: rgba(0, 212, 255, 0.32);
|
|
599
|
-
--ds-nav-hover-bg: rgba(15, 23, 42, 0.06);
|
|
600
|
-
--ds-code-bg: #f7f9fb;
|
|
601
|
-
--ds-code-border: #eef2f7;
|
|
602
|
-
--sandbox-bg: #f5f7fb;
|
|
603
|
-
--sandbox-text: #111;
|
|
604
|
-
}
|
|
605
|
-
:root[data-theme='dark'] {
|
|
606
|
-
color-scheme: dark;
|
|
607
|
-
--ds-accent: #00d4ff;
|
|
608
|
-
--ds-accent-2: #a855f7;
|
|
609
|
-
--ds-panel-bg: rgba(17, 19, 28, 0.82);
|
|
610
|
-
--ds-panel-border: rgba(255, 255, 255, 0.14);
|
|
611
|
-
--ds-subtle-bg: rgba(255, 255, 255, 0.04);
|
|
612
|
-
--ds-selected-bg: linear-gradient(90deg, rgba(0, 212, 255, 0.18), rgba(168, 85, 247, 0.14));
|
|
613
|
-
--ds-focus-ring: rgba(0, 212, 255, 0.5);
|
|
614
|
-
--ds-nav-hover-bg: rgba(255, 255, 255, 0.08);
|
|
615
|
-
--ds-code-bg: #0d1117;
|
|
616
|
-
--ds-code-border: #30363d;
|
|
617
|
-
--sandbox-bg: #0f1115;
|
|
618
|
-
--sandbox-text: #eee;
|
|
619
|
-
}
|
|
620
|
-
body {
|
|
621
|
-
margin:0;
|
|
622
|
-
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial;
|
|
623
|
-
background: var(--sandbox-bg);
|
|
624
|
-
color: var(--sandbox-text);
|
|
625
|
-
}
|
|
626
|
-
#appRoot { height: 100vh; display:flex; flex-direction:column; }
|
|
627
|
-
#sandboxToolbar {
|
|
628
|
-
flex: 0 0 auto;
|
|
629
|
-
border-bottom: 1px solid var(--ds-panel-border);
|
|
630
|
-
padding: 10px 12px;
|
|
631
|
-
background: var(--ds-panel-bg);
|
|
632
|
-
}
|
|
633
|
-
#headerSlot {
|
|
634
|
-
flex: 0 0 auto;
|
|
635
|
-
border-bottom: 1px solid var(--ds-panel-border);
|
|
636
|
-
padding: 10px 12px;
|
|
637
|
-
background: var(--ds-panel-bg);
|
|
638
|
-
}
|
|
639
|
-
#container { flex: 1 1 auto; min-height:0; overflow:hidden; }
|
|
640
|
-
#containerInner { height:100%; overflow:auto; }
|
|
641
|
-
.muted { opacity: 0.7; font-size: 12px; }
|
|
642
|
-
.bar { display:flex; gap:10px; align-items:center; justify-content:space-between; }
|
|
643
|
-
.btn {
|
|
644
|
-
border:1px solid var(--ds-panel-border);
|
|
645
|
-
background: var(--ds-subtle-bg);
|
|
646
|
-
padding:6px 10px;
|
|
647
|
-
border-radius:10px;
|
|
648
|
-
cursor:pointer;
|
|
649
|
-
font-weight:650;
|
|
650
|
-
color: inherit;
|
|
651
|
-
}
|
|
652
|
-
.btn[data-active='1'] {
|
|
653
|
-
background: var(--ds-selected-bg);
|
|
654
|
-
box-shadow: 0 0 0 2px var(--ds-focus-ring);
|
|
655
|
-
}
|
|
656
|
-
.btn:active { transform: translateY(1px); }
|
|
657
|
-
#promptsPanel {
|
|
658
|
-
position: fixed;
|
|
659
|
-
right: 12px;
|
|
660
|
-
bottom: 12px;
|
|
661
|
-
width: 420px;
|
|
662
|
-
max-height: 70vh;
|
|
663
|
-
display:none;
|
|
664
|
-
flex-direction:column;
|
|
665
|
-
background: var(--ds-panel-bg);
|
|
666
|
-
color: inherit;
|
|
667
|
-
border:1px solid var(--ds-panel-border);
|
|
668
|
-
border-radius:14px;
|
|
669
|
-
overflow:hidden;
|
|
670
|
-
box-shadow: 0 18px 60px rgba(0,0,0,0.18);
|
|
671
|
-
}
|
|
672
|
-
#promptsPanelHeader { padding: 10px 12px; display:flex; align-items:center; justify-content:space-between; border-bottom: 1px solid var(--ds-panel-border); }
|
|
673
|
-
#promptsPanelBody { padding: 10px 12px; overflow:auto; display:flex; flex-direction:column; gap:10px; }
|
|
674
|
-
#promptsFab { position: fixed; right: 16px; bottom: 16px; width: 44px; height: 44px; border-radius: 999px; display:flex; align-items:center; justify-content:center; }
|
|
675
|
-
.card { border: 1px solid var(--ds-panel-border); border-radius: 12px; padding: 10px; background: var(--ds-panel-bg); }
|
|
676
|
-
.row { display:flex; gap:10px; }
|
|
677
|
-
.toolbar-group { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
|
|
678
|
-
.segmented { display:flex; gap:6px; align-items:center; }
|
|
679
|
-
#sandboxInspector {
|
|
680
|
-
position: fixed;
|
|
681
|
-
right: 12px;
|
|
682
|
-
top: 72px;
|
|
683
|
-
width: 360px;
|
|
684
|
-
max-height: 70vh;
|
|
685
|
-
display: none;
|
|
686
|
-
flex-direction: column;
|
|
687
|
-
background: var(--ds-panel-bg);
|
|
688
|
-
border: 1px solid var(--ds-panel-border);
|
|
689
|
-
border-radius: 12px;
|
|
690
|
-
overflow: hidden;
|
|
691
|
-
box-shadow: 0 14px 40px rgba(0,0,0,0.16);
|
|
692
|
-
z-index: 10;
|
|
693
|
-
}
|
|
694
|
-
#sandboxInspectorHeader {
|
|
695
|
-
padding: 10px 12px;
|
|
696
|
-
display:flex;
|
|
697
|
-
align-items:center;
|
|
698
|
-
justify-content: space-between;
|
|
699
|
-
border-bottom: 1px solid var(--ds-panel-border);
|
|
700
|
-
}
|
|
471
|
+
if (ext === '.html') return 'text/html; charset=utf-8';
|
|
472
|
+
if (ext === '.css') return 'text/css; charset=utf-8';
|
|
473
|
+
if (ext === '.mjs' || ext === '.js') return 'text/javascript; charset=utf-8';
|
|
474
|
+
if (ext === '.json') return 'application/json; charset=utf-8';
|
|
475
|
+
if (ext === '.md') return 'text/markdown; charset=utf-8';
|
|
476
|
+
if (ext === '.svg') return 'image/svg+xml';
|
|
477
|
+
if (ext === '.png') return 'image/png';
|
|
478
|
+
return 'application/octet-stream';
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function serveStaticFile(res, filePath) {
|
|
482
|
+
if (!isFile(filePath)) return false;
|
|
483
|
+
const ct = guessContentType(filePath);
|
|
484
|
+
const buf = fs.readFileSync(filePath);
|
|
485
|
+
res.writeHead(200, { 'content-type': ct, 'cache-control': 'no-store' });
|
|
486
|
+
res.end(buf);
|
|
487
|
+
return true;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function startRecursiveWatcher(rootDir, onChange) {
|
|
491
|
+
const root = path.resolve(rootDir);
|
|
492
|
+
if (!isDirectory(root)) return () => {};
|
|
493
|
+
|
|
494
|
+
const watchers = new Map();
|
|
495
|
+
|
|
496
|
+
const shouldIgnore = (p) => {
|
|
497
|
+
const base = path.basename(p);
|
|
498
|
+
if (!base) return false;
|
|
499
|
+
if (base === 'node_modules') return true;
|
|
500
|
+
if (base === '.git') return true;
|
|
501
|
+
if (base === '.DS_Store') return true;
|
|
502
|
+
return false;
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
const scan = (dir) => {
|
|
506
|
+
const abs = path.resolve(dir);
|
|
507
|
+
if (!isDirectory(abs)) return;
|
|
508
|
+
if (shouldIgnore(abs)) return;
|
|
509
|
+
if (!watchers.has(abs)) {
|
|
510
|
+
try {
|
|
511
|
+
const w = fs.watch(abs, (eventType, filename) => {
|
|
512
|
+
const relName = filename ? String(filename) : '';
|
|
513
|
+
const filePath = relName ? path.join(abs, relName) : abs;
|
|
514
|
+
try {
|
|
515
|
+
onChange({ eventType, filePath });
|
|
516
|
+
} catch {
|
|
517
|
+
// ignore
|
|
518
|
+
}
|
|
519
|
+
scheduleRescan();
|
|
520
|
+
});
|
|
521
|
+
watchers.set(abs, w);
|
|
522
|
+
} catch {
|
|
523
|
+
// ignore
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
let entries = [];
|
|
528
|
+
try {
|
|
529
|
+
entries = fs.readdirSync(abs, { withFileTypes: true });
|
|
530
|
+
} catch {
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
for (const ent of entries) {
|
|
534
|
+
if (!ent?.isDirectory?.()) continue;
|
|
535
|
+
const child = path.join(abs, ent.name);
|
|
536
|
+
if (shouldIgnore(child)) continue;
|
|
537
|
+
scan(child);
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
let rescanTimer = null;
|
|
542
|
+
const scheduleRescan = () => {
|
|
543
|
+
if (rescanTimer) return;
|
|
544
|
+
rescanTimer = setTimeout(() => {
|
|
545
|
+
rescanTimer = null;
|
|
546
|
+
scan(root);
|
|
547
|
+
}, 250);
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
scan(root);
|
|
551
|
+
|
|
552
|
+
return () => {
|
|
553
|
+
if (rescanTimer) {
|
|
554
|
+
try {
|
|
555
|
+
clearTimeout(rescanTimer);
|
|
556
|
+
} catch {
|
|
557
|
+
// ignore
|
|
558
|
+
}
|
|
559
|
+
rescanTimer = null;
|
|
560
|
+
}
|
|
561
|
+
for (const w of watchers.values()) {
|
|
562
|
+
try {
|
|
563
|
+
w.close();
|
|
564
|
+
} catch {
|
|
565
|
+
// ignore
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
watchers.clear();
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function htmlPage() {
|
|
573
|
+
return `<!doctype html>
|
|
574
|
+
<html lang="zh-CN">
|
|
575
|
+
<head>
|
|
576
|
+
<meta charset="UTF-8" />
|
|
577
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
578
|
+
<title>ChatOS UI Apps Sandbox</title>
|
|
579
|
+
<style>
|
|
580
|
+
:root {
|
|
581
|
+
color-scheme: light;
|
|
582
|
+
--ds-accent: #00d4ff;
|
|
583
|
+
--ds-accent-2: #7c3aed;
|
|
584
|
+
--ds-panel-bg: rgba(255, 255, 255, 0.86);
|
|
585
|
+
--ds-panel-border: rgba(15, 23, 42, 0.08);
|
|
586
|
+
--ds-subtle-bg: rgba(255, 255, 255, 0.62);
|
|
587
|
+
--ds-selected-bg: linear-gradient(90deg, rgba(0, 212, 255, 0.14), rgba(124, 58, 237, 0.08));
|
|
588
|
+
--ds-focus-ring: rgba(0, 212, 255, 0.32);
|
|
589
|
+
--ds-nav-hover-bg: rgba(15, 23, 42, 0.06);
|
|
590
|
+
--ds-code-bg: #f7f9fb;
|
|
591
|
+
--ds-code-border: #eef2f7;
|
|
592
|
+
--sandbox-bg: #f5f7fb;
|
|
593
|
+
--sandbox-text: #111;
|
|
594
|
+
}
|
|
595
|
+
:root[data-theme='dark'] {
|
|
596
|
+
color-scheme: dark;
|
|
597
|
+
--ds-accent: #00d4ff;
|
|
598
|
+
--ds-accent-2: #a855f7;
|
|
599
|
+
--ds-panel-bg: rgba(17, 19, 28, 0.82);
|
|
600
|
+
--ds-panel-border: rgba(255, 255, 255, 0.14);
|
|
601
|
+
--ds-subtle-bg: rgba(255, 255, 255, 0.04);
|
|
602
|
+
--ds-selected-bg: linear-gradient(90deg, rgba(0, 212, 255, 0.18), rgba(168, 85, 247, 0.14));
|
|
603
|
+
--ds-focus-ring: rgba(0, 212, 255, 0.5);
|
|
604
|
+
--ds-nav-hover-bg: rgba(255, 255, 255, 0.08);
|
|
605
|
+
--ds-code-bg: #0d1117;
|
|
606
|
+
--ds-code-border: #30363d;
|
|
607
|
+
--sandbox-bg: #0f1115;
|
|
608
|
+
--sandbox-text: #eee;
|
|
609
|
+
}
|
|
610
|
+
body {
|
|
611
|
+
margin:0;
|
|
612
|
+
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial;
|
|
613
|
+
background: var(--sandbox-bg);
|
|
614
|
+
color: var(--sandbox-text);
|
|
615
|
+
}
|
|
616
|
+
#appRoot { height: 100vh; display:flex; flex-direction:column; }
|
|
617
|
+
#sandboxToolbar {
|
|
618
|
+
flex: 0 0 auto;
|
|
619
|
+
border-bottom: 1px solid var(--ds-panel-border);
|
|
620
|
+
padding: 10px 12px;
|
|
621
|
+
background: var(--ds-panel-bg);
|
|
622
|
+
}
|
|
623
|
+
#headerSlot {
|
|
624
|
+
flex: 0 0 auto;
|
|
625
|
+
border-bottom: 1px solid var(--ds-panel-border);
|
|
626
|
+
padding: 10px 12px;
|
|
627
|
+
background: var(--ds-panel-bg);
|
|
628
|
+
}
|
|
629
|
+
#container { flex: 1 1 auto; min-height:0; overflow:hidden; }
|
|
630
|
+
#containerInner { height:100%; overflow:auto; }
|
|
631
|
+
.muted { opacity: 0.7; font-size: 12px; }
|
|
632
|
+
.bar { display:flex; gap:10px; align-items:center; justify-content:space-between; }
|
|
633
|
+
.btn {
|
|
634
|
+
border:1px solid var(--ds-panel-border);
|
|
635
|
+
background: var(--ds-subtle-bg);
|
|
636
|
+
padding:6px 10px;
|
|
637
|
+
border-radius:10px;
|
|
638
|
+
cursor:pointer;
|
|
639
|
+
font-weight:650;
|
|
640
|
+
color: inherit;
|
|
641
|
+
}
|
|
642
|
+
.btn[data-active='1'] {
|
|
643
|
+
background: var(--ds-selected-bg);
|
|
644
|
+
box-shadow: 0 0 0 2px var(--ds-focus-ring);
|
|
645
|
+
}
|
|
646
|
+
.btn:active { transform: translateY(1px); }
|
|
647
|
+
#promptsPanel {
|
|
648
|
+
position: fixed;
|
|
649
|
+
right: 12px;
|
|
650
|
+
bottom: 12px;
|
|
651
|
+
width: 420px;
|
|
652
|
+
max-height: 70vh;
|
|
653
|
+
display:none;
|
|
654
|
+
flex-direction:column;
|
|
655
|
+
background: var(--ds-panel-bg);
|
|
656
|
+
color: inherit;
|
|
657
|
+
border:1px solid var(--ds-panel-border);
|
|
658
|
+
border-radius:14px;
|
|
659
|
+
overflow:hidden;
|
|
660
|
+
box-shadow: 0 18px 60px rgba(0,0,0,0.18);
|
|
661
|
+
}
|
|
662
|
+
#promptsPanelHeader { padding: 10px 12px; display:flex; align-items:center; justify-content:space-between; border-bottom: 1px solid var(--ds-panel-border); }
|
|
663
|
+
#promptsPanelBody { padding: 10px 12px; overflow:auto; display:flex; flex-direction:column; gap:10px; }
|
|
664
|
+
#promptsFab { position: fixed; right: 16px; bottom: 16px; width: 44px; height: 44px; border-radius: 999px; display:flex; align-items:center; justify-content:center; }
|
|
665
|
+
.card { border: 1px solid var(--ds-panel-border); border-radius: 12px; padding: 10px; background: var(--ds-panel-bg); }
|
|
666
|
+
.row { display:flex; gap:10px; }
|
|
667
|
+
.toolbar-group { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
|
|
668
|
+
.segmented { display:flex; gap:6px; align-items:center; }
|
|
669
|
+
#sandboxInspector {
|
|
670
|
+
position: fixed;
|
|
671
|
+
right: 12px;
|
|
672
|
+
top: 72px;
|
|
673
|
+
width: 360px;
|
|
674
|
+
max-height: 70vh;
|
|
675
|
+
display: none;
|
|
676
|
+
flex-direction: column;
|
|
677
|
+
background: var(--ds-panel-bg);
|
|
678
|
+
border: 1px solid var(--ds-panel-border);
|
|
679
|
+
border-radius: 12px;
|
|
680
|
+
overflow: hidden;
|
|
681
|
+
box-shadow: 0 14px 40px rgba(0,0,0,0.16);
|
|
682
|
+
z-index: 10;
|
|
683
|
+
}
|
|
684
|
+
#sandboxInspectorHeader {
|
|
685
|
+
padding: 10px 12px;
|
|
686
|
+
display:flex;
|
|
687
|
+
align-items:center;
|
|
688
|
+
justify-content: space-between;
|
|
689
|
+
border-bottom: 1px solid var(--ds-panel-border);
|
|
690
|
+
}
|
|
701
691
|
#sandboxInspectorBody {
|
|
702
692
|
padding: 10px 12px;
|
|
703
693
|
overflow: auto;
|
|
@@ -734,50 +724,88 @@ function htmlPage() {
|
|
|
734
724
|
flex-direction: column;
|
|
735
725
|
gap: 10px;
|
|
736
726
|
}
|
|
727
|
+
#mcpPanel {
|
|
728
|
+
position: fixed;
|
|
729
|
+
right: 12px;
|
|
730
|
+
top: 72px;
|
|
731
|
+
width: 520px;
|
|
732
|
+
max-height: 70vh;
|
|
733
|
+
display: none;
|
|
734
|
+
flex-direction: column;
|
|
735
|
+
background: var(--ds-panel-bg);
|
|
736
|
+
border: 1px solid var(--ds-panel-border);
|
|
737
|
+
border-radius: 12px;
|
|
738
|
+
overflow: hidden;
|
|
739
|
+
box-shadow: 0 14px 40px rgba(0,0,0,0.16);
|
|
740
|
+
z-index: 12;
|
|
741
|
+
}
|
|
742
|
+
#mcpPanelHeader {
|
|
743
|
+
padding: 10px 12px;
|
|
744
|
+
display:flex;
|
|
745
|
+
align-items:center;
|
|
746
|
+
justify-content: space-between;
|
|
747
|
+
border-bottom: 1px solid var(--ds-panel-border);
|
|
748
|
+
}
|
|
749
|
+
#mcpPanelBody {
|
|
750
|
+
padding: 10px 12px;
|
|
751
|
+
overflow: auto;
|
|
752
|
+
display: flex;
|
|
753
|
+
flex-direction: column;
|
|
754
|
+
gap: 10px;
|
|
755
|
+
}
|
|
756
|
+
#mcpPaths {
|
|
757
|
+
max-height: 140px;
|
|
758
|
+
overflow: auto;
|
|
759
|
+
}
|
|
760
|
+
#mcpOutput {
|
|
761
|
+
max-height: 240px;
|
|
762
|
+
overflow: auto;
|
|
763
|
+
}
|
|
737
764
|
.section-title { font-size: 12px; font-weight: 700; opacity: 0.8; }
|
|
738
765
|
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 12px; white-space: pre-wrap; }
|
|
739
766
|
input, textarea, select {
|
|
740
767
|
width:100%;
|
|
741
|
-
padding:8px;
|
|
742
|
-
border-radius:10px;
|
|
743
|
-
border:1px solid var(--ds-panel-border);
|
|
744
|
-
background: var(--ds-subtle-bg);
|
|
745
|
-
color: inherit;
|
|
746
|
-
}
|
|
747
|
-
textarea { min-height: 70px; resize: vertical; }
|
|
748
|
-
label { font-size: 12px; opacity: 0.8; }
|
|
749
|
-
.danger { border-color: rgba(255,0,0,0.35); }
|
|
750
|
-
</style>
|
|
751
|
-
</head>
|
|
752
|
-
<body>
|
|
753
|
-
<div id="appRoot">
|
|
754
|
-
<div id="sandboxToolbar">
|
|
755
|
-
<div class="bar">
|
|
756
|
-
<div>
|
|
757
|
-
<div style="font-weight:800">ChatOS UI Apps Sandbox</div>
|
|
758
|
-
<div class="muted">Host API mock · 模拟 module mount({ container, host, slots })</div>
|
|
759
|
-
</div>
|
|
760
|
-
<div class="row toolbar-group">
|
|
761
|
-
<span class="muted">Theme</span>
|
|
762
|
-
<div class="segmented" role="group" aria-label="Theme">
|
|
763
|
-
<button id="btnThemeLight" class="btn" type="button">Light</button>
|
|
764
|
-
<button id="btnThemeDark" class="btn" type="button">Dark</button>
|
|
765
|
-
<button id="btnThemeSystem" class="btn" type="button">System</button>
|
|
766
|
-
</div>
|
|
768
|
+
padding:8px;
|
|
769
|
+
border-radius:10px;
|
|
770
|
+
border:1px solid var(--ds-panel-border);
|
|
771
|
+
background: var(--ds-subtle-bg);
|
|
772
|
+
color: inherit;
|
|
773
|
+
}
|
|
774
|
+
textarea { min-height: 70px; resize: vertical; }
|
|
775
|
+
label { font-size: 12px; opacity: 0.8; }
|
|
776
|
+
.danger { border-color: rgba(255,0,0,0.35); }
|
|
777
|
+
</style>
|
|
778
|
+
</head>
|
|
779
|
+
<body>
|
|
780
|
+
<div id="appRoot">
|
|
781
|
+
<div id="sandboxToolbar">
|
|
782
|
+
<div class="bar">
|
|
783
|
+
<div>
|
|
784
|
+
<div style="font-weight:800">ChatOS UI Apps Sandbox</div>
|
|
785
|
+
<div class="muted">Host API mock · 模拟 module mount({ container, host, slots })</div>
|
|
786
|
+
</div>
|
|
787
|
+
<div class="row toolbar-group">
|
|
788
|
+
<span class="muted">Theme</span>
|
|
789
|
+
<div class="segmented" role="group" aria-label="Theme">
|
|
790
|
+
<button id="btnThemeLight" class="btn" type="button">Light</button>
|
|
791
|
+
<button id="btnThemeDark" class="btn" type="button">Dark</button>
|
|
792
|
+
<button id="btnThemeSystem" class="btn" type="button">System</button>
|
|
793
|
+
</div>
|
|
767
794
|
<div id="themeStatus" class="muted"></div>
|
|
768
795
|
<div id="sandboxContext" class="muted"></div>
|
|
769
796
|
<button id="btnLlmConfig" class="btn" type="button">AI Config</button>
|
|
797
|
+
<button id="btnMcpTest" class="btn" type="button">MCP Test</button>
|
|
770
798
|
<button id="btnInspectorToggle" class="btn" type="button">Inspect</button>
|
|
771
799
|
<button id="btnReload" class="btn" type="button">Reload</button>
|
|
772
800
|
</div>
|
|
773
801
|
</div>
|
|
774
802
|
</div>
|
|
775
|
-
<div id="headerSlot"></div>
|
|
776
|
-
<div id="container"><div id="containerInner"></div></div>
|
|
777
|
-
</div>
|
|
778
|
-
|
|
779
|
-
<button id="promptsFab" class="btn" type="button">:)</button>
|
|
780
|
-
|
|
803
|
+
<div id="headerSlot"></div>
|
|
804
|
+
<div id="container"><div id="containerInner"></div></div>
|
|
805
|
+
</div>
|
|
806
|
+
|
|
807
|
+
<button id="promptsFab" class="btn" type="button">:)</button>
|
|
808
|
+
|
|
781
809
|
<div id="promptsPanel">
|
|
782
810
|
<div id="promptsPanelHeader">
|
|
783
811
|
<div style="font-weight:800">UI Prompts</div>
|
|
@@ -808,11 +836,6 @@ function htmlPage() {
|
|
|
808
836
|
<label for="llmModelId">Model ID</label>
|
|
809
837
|
<input id="llmModelId" type="text" placeholder="gpt-4o-mini" />
|
|
810
838
|
</div>
|
|
811
|
-
<div class="card">
|
|
812
|
-
<label for="llmWorkdir">Workdir</label>
|
|
813
|
-
<input id="llmWorkdir" type="text" placeholder="(default: dataDir)" />
|
|
814
|
-
<div class="muted">留空使用 dataDir;支持 $dataDir/$pluginDir/$projectRoot</div>
|
|
815
|
-
</div>
|
|
816
839
|
<div class="row">
|
|
817
840
|
<button id="btnLlmSave" class="btn" type="button">Save</button>
|
|
818
841
|
<button id="btnLlmClear" class="btn" type="button">Clear Key</button>
|
|
@@ -821,51 +844,95 @@ function htmlPage() {
|
|
|
821
844
|
</div>
|
|
822
845
|
</div>
|
|
823
846
|
|
|
847
|
+
<div id="mcpPanel" aria-hidden="true">
|
|
848
|
+
<div id="mcpPanelHeader">
|
|
849
|
+
<div style="font-weight:800">MCP Test</div>
|
|
850
|
+
<div class="row">
|
|
851
|
+
<button id="btnMcpClear" class="btn" type="button">Clear</button>
|
|
852
|
+
<button id="btnMcpClose" class="btn" type="button">Close</button>
|
|
853
|
+
</div>
|
|
854
|
+
</div>
|
|
855
|
+
<div id="mcpPanelBody">
|
|
856
|
+
<div class="card">
|
|
857
|
+
<div class="section-title">Paths</div>
|
|
858
|
+
<pre id="mcpPaths" class="mono"></pre>
|
|
859
|
+
<label for="mcpWorkdir">Workdir Override (optional)</label>
|
|
860
|
+
<input id="mcpWorkdir" type="text" placeholder="留空则使用默认 workdir" />
|
|
861
|
+
</div>
|
|
862
|
+
<div class="card">
|
|
863
|
+
<div class="section-title">Model</div>
|
|
864
|
+
<label for="mcpModelId">Model ID (optional)</label>
|
|
865
|
+
<input id="mcpModelId" type="text" placeholder="留空则使用 AI Config 中的 Model ID" />
|
|
866
|
+
<div id="mcpConfigHint" class="muted"></div>
|
|
867
|
+
</div>
|
|
868
|
+
<div class="card">
|
|
869
|
+
<label for="mcpSystem">System Prompt (optional)</label>
|
|
870
|
+
<textarea id="mcpSystem" placeholder="留空则使用 MCP Prompt / 空"></textarea>
|
|
871
|
+
</div>
|
|
872
|
+
<div class="card">
|
|
873
|
+
<label for="mcpMessage">User Message</label>
|
|
874
|
+
<textarea id="mcpMessage" placeholder="输入测试消息"></textarea>
|
|
875
|
+
<div class="row" style="margin-top:8px; align-items:center; justify-content:space-between;">
|
|
876
|
+
<label style="display:flex; gap:6px; align-items:center;">
|
|
877
|
+
<input id="mcpDisableTools" type="checkbox" style="width:auto;" />
|
|
878
|
+
Disable tools
|
|
879
|
+
</label>
|
|
880
|
+
<button id="btnMcpSend" class="btn" type="button">Send</button>
|
|
881
|
+
</div>
|
|
882
|
+
<div id="mcpStatus" class="muted"></div>
|
|
883
|
+
</div>
|
|
884
|
+
<div class="card">
|
|
885
|
+
<div class="section-title">Output</div>
|
|
886
|
+
<pre id="mcpOutput" class="mono"></pre>
|
|
887
|
+
</div>
|
|
888
|
+
</div>
|
|
889
|
+
</div>
|
|
890
|
+
|
|
824
891
|
<div id="sandboxInspector" aria-hidden="true">
|
|
825
892
|
<div id="sandboxInspectorHeader">
|
|
826
893
|
<div style="font-weight:800">Sandbox Inspector</div>
|
|
827
894
|
<div class="row">
|
|
828
895
|
<button id="btnInspectorRefresh" class="btn" type="button">Refresh</button>
|
|
829
|
-
<button id="btnInspectorClose" class="btn" type="button">Close</button>
|
|
830
|
-
</div>
|
|
831
|
-
</div>
|
|
832
|
-
<div id="sandboxInspectorBody">
|
|
833
|
-
<div>
|
|
834
|
-
<div class="section-title">Host Context</div>
|
|
835
|
-
<pre id="inspectorContext" class="mono"></pre>
|
|
836
|
-
</div>
|
|
837
|
-
<div>
|
|
838
|
-
<div class="section-title">Theme</div>
|
|
839
|
-
<pre id="inspectorTheme" class="mono"></pre>
|
|
840
|
-
</div>
|
|
841
|
-
<div>
|
|
842
|
-
<div class="section-title">Tokens</div>
|
|
843
|
-
<pre id="inspectorTokens" class="mono"></pre>
|
|
844
|
-
</div>
|
|
845
|
-
</div>
|
|
846
|
-
</div>
|
|
847
|
-
|
|
848
|
-
<script type="module" src="/sandbox.mjs"></script>
|
|
849
|
-
</body>
|
|
850
|
-
</html>`;
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
function sandboxClientJs() {
|
|
854
|
-
return `const $ = (sel) => document.querySelector(sel);
|
|
855
|
-
|
|
856
|
-
const container = $('#containerInner');
|
|
857
|
-
const headerSlot = $('#headerSlot');
|
|
858
|
-
const fab = $('#promptsFab');
|
|
859
|
-
const panel = $('#promptsPanel');
|
|
860
|
-
const panelBody = $('#promptsPanelBody');
|
|
861
|
-
const panelClose = $('#promptsClose');
|
|
862
|
-
const btnThemeLight = $('#btnThemeLight');
|
|
863
|
-
const btnThemeDark = $('#btnThemeDark');
|
|
864
|
-
const btnThemeSystem = $('#btnThemeSystem');
|
|
865
|
-
const themeStatus = $('#themeStatus');
|
|
866
|
-
const sandboxContext = $('#sandboxContext');
|
|
867
|
-
const btnInspectorToggle = $('#btnInspectorToggle');
|
|
868
|
-
const sandboxInspector = $('#sandboxInspector');
|
|
896
|
+
<button id="btnInspectorClose" class="btn" type="button">Close</button>
|
|
897
|
+
</div>
|
|
898
|
+
</div>
|
|
899
|
+
<div id="sandboxInspectorBody">
|
|
900
|
+
<div>
|
|
901
|
+
<div class="section-title">Host Context</div>
|
|
902
|
+
<pre id="inspectorContext" class="mono"></pre>
|
|
903
|
+
</div>
|
|
904
|
+
<div>
|
|
905
|
+
<div class="section-title">Theme</div>
|
|
906
|
+
<pre id="inspectorTheme" class="mono"></pre>
|
|
907
|
+
</div>
|
|
908
|
+
<div>
|
|
909
|
+
<div class="section-title">Tokens</div>
|
|
910
|
+
<pre id="inspectorTokens" class="mono"></pre>
|
|
911
|
+
</div>
|
|
912
|
+
</div>
|
|
913
|
+
</div>
|
|
914
|
+
|
|
915
|
+
<script type="module" src="/sandbox.mjs"></script>
|
|
916
|
+
</body>
|
|
917
|
+
</html>`;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function sandboxClientJs() {
|
|
921
|
+
return `const $ = (sel) => document.querySelector(sel);
|
|
922
|
+
|
|
923
|
+
const container = $('#containerInner');
|
|
924
|
+
const headerSlot = $('#headerSlot');
|
|
925
|
+
const fab = $('#promptsFab');
|
|
926
|
+
const panel = $('#promptsPanel');
|
|
927
|
+
const panelBody = $('#promptsPanelBody');
|
|
928
|
+
const panelClose = $('#promptsClose');
|
|
929
|
+
const btnThemeLight = $('#btnThemeLight');
|
|
930
|
+
const btnThemeDark = $('#btnThemeDark');
|
|
931
|
+
const btnThemeSystem = $('#btnThemeSystem');
|
|
932
|
+
const themeStatus = $('#themeStatus');
|
|
933
|
+
const sandboxContext = $('#sandboxContext');
|
|
934
|
+
const btnInspectorToggle = $('#btnInspectorToggle');
|
|
935
|
+
const sandboxInspector = $('#sandboxInspector');
|
|
869
936
|
const btnInspectorClose = $('#btnInspectorClose');
|
|
870
937
|
const btnInspectorRefresh = $('#btnInspectorRefresh');
|
|
871
938
|
const inspectorContext = $('#inspectorContext');
|
|
@@ -880,62 +947,75 @@ const btnLlmClear = $('#btnLlmClear');
|
|
|
880
947
|
const llmApiKey = $('#llmApiKey');
|
|
881
948
|
const llmBaseUrl = $('#llmBaseUrl');
|
|
882
949
|
const llmModelId = $('#llmModelId');
|
|
883
|
-
const llmWorkdir = $('#llmWorkdir');
|
|
884
950
|
const llmStatus = $('#llmStatus');
|
|
885
951
|
const llmKeyStatus = $('#llmKeyStatus');
|
|
952
|
+
const btnMcpTest = $('#btnMcpTest');
|
|
953
|
+
const mcpPanel = $('#mcpPanel');
|
|
954
|
+
const btnMcpClose = $('#btnMcpClose');
|
|
955
|
+
const btnMcpClear = $('#btnMcpClear');
|
|
956
|
+
const btnMcpSend = $('#btnMcpSend');
|
|
957
|
+
const mcpPaths = $('#mcpPaths');
|
|
958
|
+
const mcpWorkdir = $('#mcpWorkdir');
|
|
959
|
+
const mcpModelId = $('#mcpModelId');
|
|
960
|
+
const mcpSystem = $('#mcpSystem');
|
|
961
|
+
const mcpMessage = $('#mcpMessage');
|
|
962
|
+
const mcpDisableTools = $('#mcpDisableTools');
|
|
963
|
+
const mcpStatus = $('#mcpStatus');
|
|
964
|
+
const mcpOutput = $('#mcpOutput');
|
|
965
|
+
const mcpConfigHint = $('#mcpConfigHint');
|
|
886
966
|
|
|
887
967
|
const setPanelOpen = (open) => { panel.style.display = open ? 'flex' : 'none'; };
|
|
888
968
|
fab.addEventListener('click', () => setPanelOpen(panel.style.display !== 'flex'));
|
|
889
969
|
panelClose.addEventListener('click', () => setPanelOpen(false));
|
|
890
970
|
window.addEventListener('chatos:uiPrompts:open', () => setPanelOpen(true));
|
|
891
|
-
window.addEventListener('chatos:uiPrompts:close', () => setPanelOpen(false));
|
|
892
|
-
window.addEventListener('chatos:uiPrompts:toggle', () => setPanelOpen(panel.style.display !== 'flex'));
|
|
893
|
-
|
|
894
|
-
const THEME_STORAGE_KEY = 'chatos:sandbox:theme-mode';
|
|
895
|
-
const themeListeners = new Set();
|
|
896
|
-
const themeButtons = [
|
|
897
|
-
{ mode: 'light', el: btnThemeLight },
|
|
898
|
-
{ mode: 'dark', el: btnThemeDark },
|
|
899
|
-
{ mode: 'system', el: btnThemeSystem },
|
|
900
|
-
];
|
|
901
|
-
const systemQuery = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null;
|
|
902
|
-
|
|
903
|
-
const normalizeThemeMode = (mode) => (mode === 'light' || mode === 'dark' || mode === 'system' ? mode : 'system');
|
|
904
|
-
|
|
905
|
-
const loadThemeMode = () => {
|
|
906
|
-
try {
|
|
907
|
-
return normalizeThemeMode(String(localStorage.getItem(THEME_STORAGE_KEY) || ''));
|
|
908
|
-
} catch {
|
|
909
|
-
return 'system';
|
|
910
|
-
}
|
|
911
|
-
};
|
|
912
|
-
|
|
913
|
-
let themeMode = loadThemeMode();
|
|
914
|
-
let currentTheme = 'light';
|
|
915
|
-
let inspectorEnabled = false;
|
|
916
|
-
let inspectorTimer = null;
|
|
917
|
-
|
|
918
|
-
const resolveTheme = () => {
|
|
919
|
-
if (themeMode === 'light' || themeMode === 'dark') return themeMode;
|
|
920
|
-
return systemQuery && systemQuery.matches ? 'dark' : 'light';
|
|
921
|
-
};
|
|
922
|
-
|
|
923
|
-
const emitThemeChange = (theme) => {
|
|
924
|
-
for (const fn of themeListeners) { try { fn(theme); } catch {} }
|
|
925
|
-
};
|
|
926
|
-
|
|
927
|
-
const updateThemeControls = () => {
|
|
928
|
-
for (const { mode, el } of themeButtons) {
|
|
929
|
-
if (!el) continue;
|
|
930
|
-
const active = mode === themeMode;
|
|
931
|
-
el.dataset.active = active ? '1' : '0';
|
|
932
|
-
el.setAttribute('aria-pressed', active ? 'true' : 'false');
|
|
933
|
-
}
|
|
934
|
-
if (themeStatus) {
|
|
935
|
-
themeStatus.textContent = themeMode === 'system' ? 'system -> ' + currentTheme : currentTheme;
|
|
936
|
-
}
|
|
937
|
-
};
|
|
938
|
-
|
|
971
|
+
window.addEventListener('chatos:uiPrompts:close', () => setPanelOpen(false));
|
|
972
|
+
window.addEventListener('chatos:uiPrompts:toggle', () => setPanelOpen(panel.style.display !== 'flex'));
|
|
973
|
+
|
|
974
|
+
const THEME_STORAGE_KEY = 'chatos:sandbox:theme-mode';
|
|
975
|
+
const themeListeners = new Set();
|
|
976
|
+
const themeButtons = [
|
|
977
|
+
{ mode: 'light', el: btnThemeLight },
|
|
978
|
+
{ mode: 'dark', el: btnThemeDark },
|
|
979
|
+
{ mode: 'system', el: btnThemeSystem },
|
|
980
|
+
];
|
|
981
|
+
const systemQuery = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null;
|
|
982
|
+
|
|
983
|
+
const normalizeThemeMode = (mode) => (mode === 'light' || mode === 'dark' || mode === 'system' ? mode : 'system');
|
|
984
|
+
|
|
985
|
+
const loadThemeMode = () => {
|
|
986
|
+
try {
|
|
987
|
+
return normalizeThemeMode(String(localStorage.getItem(THEME_STORAGE_KEY) || ''));
|
|
988
|
+
} catch {
|
|
989
|
+
return 'system';
|
|
990
|
+
}
|
|
991
|
+
};
|
|
992
|
+
|
|
993
|
+
let themeMode = loadThemeMode();
|
|
994
|
+
let currentTheme = 'light';
|
|
995
|
+
let inspectorEnabled = false;
|
|
996
|
+
let inspectorTimer = null;
|
|
997
|
+
|
|
998
|
+
const resolveTheme = () => {
|
|
999
|
+
if (themeMode === 'light' || themeMode === 'dark') return themeMode;
|
|
1000
|
+
return systemQuery && systemQuery.matches ? 'dark' : 'light';
|
|
1001
|
+
};
|
|
1002
|
+
|
|
1003
|
+
const emitThemeChange = (theme) => {
|
|
1004
|
+
for (const fn of themeListeners) { try { fn(theme); } catch {} }
|
|
1005
|
+
};
|
|
1006
|
+
|
|
1007
|
+
const updateThemeControls = () => {
|
|
1008
|
+
for (const { mode, el } of themeButtons) {
|
|
1009
|
+
if (!el) continue;
|
|
1010
|
+
const active = mode === themeMode;
|
|
1011
|
+
el.dataset.active = active ? '1' : '0';
|
|
1012
|
+
el.setAttribute('aria-pressed', active ? 'true' : 'false');
|
|
1013
|
+
}
|
|
1014
|
+
if (themeStatus) {
|
|
1015
|
+
themeStatus.textContent = themeMode === 'system' ? 'system -> ' + currentTheme : currentTheme;
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
|
|
939
1019
|
const updateContextStatus = () => {
|
|
940
1020
|
if (!sandboxContext) return;
|
|
941
1021
|
sandboxContext.textContent = __SANDBOX__.pluginId + ':' + __SANDBOX__.appId;
|
|
@@ -943,6 +1023,7 @@ const updateContextStatus = () => {
|
|
|
943
1023
|
|
|
944
1024
|
const isInspectorOpen = () => sandboxInspector && sandboxInspector.style.display === 'flex';
|
|
945
1025
|
const isLlmPanelOpen = () => llmPanel && llmPanel.style.display === 'flex';
|
|
1026
|
+
const isMcpPanelOpen = () => mcpPanel && mcpPanel.style.display === 'flex';
|
|
946
1027
|
|
|
947
1028
|
const setLlmStatus = (text, isError) => {
|
|
948
1029
|
if (!llmStatus) return;
|
|
@@ -959,7 +1040,6 @@ const refreshLlmConfig = async () => {
|
|
|
959
1040
|
const cfg = j?.config || {};
|
|
960
1041
|
if (llmBaseUrl) llmBaseUrl.value = cfg.baseUrl || '';
|
|
961
1042
|
if (llmModelId) llmModelId.value = cfg.modelId || '';
|
|
962
|
-
if (llmWorkdir) llmWorkdir.value = cfg.workdir || '';
|
|
963
1043
|
if (llmKeyStatus) llmKeyStatus.textContent = cfg.hasApiKey ? 'API key set' : 'API key missing';
|
|
964
1044
|
setLlmStatus('');
|
|
965
1045
|
} catch (err) {
|
|
@@ -973,7 +1053,6 @@ const saveLlmConfig = async ({ clearKey } = {}) => {
|
|
|
973
1053
|
const payload = {
|
|
974
1054
|
baseUrl: llmBaseUrl ? llmBaseUrl.value : '',
|
|
975
1055
|
modelId: llmModelId ? llmModelId.value : '',
|
|
976
|
-
workdir: llmWorkdir ? llmWorkdir.value : '',
|
|
977
1056
|
};
|
|
978
1057
|
const apiKey = llmApiKey ? llmApiKey.value : '';
|
|
979
1058
|
if (clearKey) {
|
|
@@ -1007,103 +1086,190 @@ const formatJson = (value) => {
|
|
|
1007
1086
|
try {
|
|
1008
1087
|
return JSON.stringify(value, null, 2);
|
|
1009
1088
|
} catch {
|
|
1010
|
-
return String(value);
|
|
1011
|
-
}
|
|
1012
|
-
};
|
|
1013
|
-
|
|
1089
|
+
return String(value);
|
|
1090
|
+
}
|
|
1091
|
+
};
|
|
1092
|
+
|
|
1093
|
+
const sandboxPaths = __SANDBOX__.paths || {};
|
|
1094
|
+
|
|
1095
|
+
const setMcpStatus = (text, isError) => {
|
|
1096
|
+
if (!mcpStatus) return;
|
|
1097
|
+
mcpStatus.textContent = text || '';
|
|
1098
|
+
mcpStatus.style.color = isError ? '#ef4444' : '';
|
|
1099
|
+
};
|
|
1100
|
+
|
|
1101
|
+
const appendMcpOutput = (label, payload) => {
|
|
1102
|
+
if (!mcpOutput) return;
|
|
1103
|
+
const ts = new Date().toISOString();
|
|
1104
|
+
const content = typeof payload === 'string' ? payload : formatJson(payload);
|
|
1105
|
+
mcpOutput.textContent += '[' + ts + '] ' + label + '\\n' + content + '\\n\\n';
|
|
1106
|
+
mcpOutput.scrollTop = mcpOutput.scrollHeight;
|
|
1107
|
+
};
|
|
1108
|
+
|
|
1109
|
+
const refreshMcpPaths = () => {
|
|
1110
|
+
if (!mcpPaths) return;
|
|
1111
|
+
mcpPaths.textContent = formatJson(sandboxPaths);
|
|
1112
|
+
};
|
|
1113
|
+
|
|
1114
|
+
const refreshMcpConfigHint = async () => {
|
|
1115
|
+
if (!mcpConfigHint) return;
|
|
1116
|
+
try {
|
|
1117
|
+
mcpConfigHint.textContent = 'Loading...';
|
|
1118
|
+
const r = await fetch('/api/sandbox/llm-config');
|
|
1119
|
+
const j = await r.json();
|
|
1120
|
+
if (!j?.ok) throw new Error(j?.message || 'Failed to load config');
|
|
1121
|
+
const cfg = j?.config || {};
|
|
1122
|
+
const modelText = cfg.modelId ? 'model=' + cfg.modelId : 'model=unset';
|
|
1123
|
+
const baseText = cfg.baseUrl ? 'base=' + cfg.baseUrl : 'base=default';
|
|
1124
|
+
const keyText = cfg.hasApiKey ? 'key=ok' : 'key=missing';
|
|
1125
|
+
mcpConfigHint.textContent = modelText + ' · ' + baseText + ' · ' + keyText;
|
|
1126
|
+
} catch (err) {
|
|
1127
|
+
mcpConfigHint.textContent = err?.message || String(err);
|
|
1128
|
+
}
|
|
1129
|
+
};
|
|
1130
|
+
|
|
1131
|
+
const setMcpPanelOpen = (open) => {
|
|
1132
|
+
if (!mcpPanel) return;
|
|
1133
|
+
mcpPanel.style.display = open ? 'flex' : 'none';
|
|
1134
|
+
mcpPanel.setAttribute('aria-hidden', open ? 'false' : 'true');
|
|
1135
|
+
if (open) {
|
|
1136
|
+
refreshMcpConfigHint();
|
|
1137
|
+
refreshMcpPaths();
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
|
|
1141
|
+
const runMcpTest = async () => {
|
|
1142
|
+
const sendBtn = btnMcpSend;
|
|
1143
|
+
try {
|
|
1144
|
+
const message = mcpMessage ? String(mcpMessage.value || '').trim() : '';
|
|
1145
|
+
if (!message) {
|
|
1146
|
+
setMcpStatus('Message is required.', true);
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
if (sendBtn) sendBtn.disabled = true;
|
|
1150
|
+
const payload = {
|
|
1151
|
+
messages: [{ role: 'user', text: message }],
|
|
1152
|
+
modelId: mcpModelId ? String(mcpModelId.value || '').trim() : '',
|
|
1153
|
+
systemPrompt: mcpSystem ? String(mcpSystem.value || '').trim() : '',
|
|
1154
|
+
disableTools: Boolean(mcpDisableTools?.checked),
|
|
1155
|
+
};
|
|
1156
|
+
const workdirOverride = mcpWorkdir ? String(mcpWorkdir.value || '').trim() : '';
|
|
1157
|
+
if (workdirOverride) {
|
|
1158
|
+
payload.callMeta = { workdir: workdirOverride };
|
|
1159
|
+
}
|
|
1160
|
+
setMcpStatus('Sending...');
|
|
1161
|
+
appendMcpOutput('request', payload);
|
|
1162
|
+
const r = await fetch('/api/llm/chat', {
|
|
1163
|
+
method: 'POST',
|
|
1164
|
+
headers: { 'content-type': 'application/json' },
|
|
1165
|
+
body: JSON.stringify(payload),
|
|
1166
|
+
});
|
|
1167
|
+
const j = await r.json();
|
|
1168
|
+
if (!j?.ok) throw new Error(j?.message || 'MCP test failed');
|
|
1169
|
+
appendMcpOutput('assistant', j?.content || '');
|
|
1170
|
+
if (Array.isArray(j?.toolTrace) && j.toolTrace.length > 0) {
|
|
1171
|
+
appendMcpOutput('toolTrace', j.toolTrace);
|
|
1172
|
+
}
|
|
1173
|
+
setMcpStatus('Done');
|
|
1174
|
+
} catch (err) {
|
|
1175
|
+
setMcpStatus(err?.message || String(err), true);
|
|
1176
|
+
} finally {
|
|
1177
|
+
if (sendBtn) sendBtn.disabled = false;
|
|
1178
|
+
}
|
|
1179
|
+
};
|
|
1180
|
+
|
|
1014
1181
|
const tokenNameList = Array.isArray(__SANDBOX__.tokenNames) ? __SANDBOX__.tokenNames : [];
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
const
|
|
1018
|
-
const
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
.
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1182
|
+
|
|
1183
|
+
const collectTokens = () => {
|
|
1184
|
+
const style = getComputedStyle(document.documentElement);
|
|
1185
|
+
const names = new Set(tokenNameList);
|
|
1186
|
+
for (let i = 0; i < style.length; i += 1) {
|
|
1187
|
+
const name = style[i];
|
|
1188
|
+
if (name && name.startsWith('--ds-')) names.add(name);
|
|
1189
|
+
}
|
|
1190
|
+
return [...names]
|
|
1191
|
+
.sort()
|
|
1192
|
+
.map((name) => {
|
|
1193
|
+
const value = style.getPropertyValue(name).trim();
|
|
1194
|
+
return name + ': ' + (value || '(unset)');
|
|
1195
|
+
})
|
|
1196
|
+
.join('\\n');
|
|
1197
|
+
};
|
|
1198
|
+
|
|
1033
1199
|
const readHostContext = () => {
|
|
1034
1200
|
if (!inspectorEnabled) return null;
|
|
1035
1201
|
if (typeof host?.context?.get === 'function') return host.context.get();
|
|
1036
|
-
return {
|
|
1202
|
+
return { pluginId: __SANDBOX__.pluginId, appId: __SANDBOX__.appId, theme: currentTheme, bridge: { enabled: true } };
|
|
1203
|
+
};
|
|
1204
|
+
|
|
1205
|
+
const readThemeInfo = () => ({
|
|
1206
|
+
themeMode,
|
|
1207
|
+
currentTheme,
|
|
1208
|
+
dataTheme: document.documentElement.dataset.theme || '',
|
|
1209
|
+
dataThemeMode: document.documentElement.dataset.themeMode || '',
|
|
1210
|
+
prefersColorScheme: systemQuery ? (systemQuery.matches ? 'dark' : 'light') : 'unknown',
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
const updateInspector = () => {
|
|
1214
|
+
if (!inspectorEnabled) return;
|
|
1215
|
+
if (inspectorContext) inspectorContext.textContent = formatJson(readHostContext());
|
|
1216
|
+
if (inspectorTheme) inspectorTheme.textContent = formatJson(readThemeInfo());
|
|
1217
|
+
if (inspectorTokens) inspectorTokens.textContent = collectTokens();
|
|
1218
|
+
};
|
|
1219
|
+
|
|
1220
|
+
const startInspectorTimer = () => {
|
|
1221
|
+
if (inspectorTimer) return;
|
|
1222
|
+
inspectorTimer = setInterval(updateInspector, 1000);
|
|
1223
|
+
};
|
|
1224
|
+
|
|
1225
|
+
const stopInspectorTimer = () => {
|
|
1226
|
+
if (!inspectorTimer) return;
|
|
1227
|
+
clearInterval(inspectorTimer);
|
|
1228
|
+
inspectorTimer = null;
|
|
1229
|
+
};
|
|
1230
|
+
|
|
1231
|
+
const setInspectorOpen = (open) => {
|
|
1232
|
+
if (!sandboxInspector) return;
|
|
1233
|
+
sandboxInspector.style.display = open ? 'flex' : 'none';
|
|
1234
|
+
sandboxInspector.setAttribute('aria-hidden', open ? 'false' : 'true');
|
|
1235
|
+
if (open) {
|
|
1236
|
+
updateInspector();
|
|
1237
|
+
startInspectorTimer();
|
|
1238
|
+
} else {
|
|
1239
|
+
stopInspectorTimer();
|
|
1240
|
+
}
|
|
1037
1241
|
};
|
|
1038
|
-
|
|
1039
|
-
const
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
const
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
if (
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
if (open) {
|
|
1070
|
-
updateInspector();
|
|
1071
|
-
startInspectorTimer();
|
|
1072
|
-
} else {
|
|
1073
|
-
stopInspectorTimer();
|
|
1074
|
-
}
|
|
1075
|
-
};
|
|
1076
|
-
|
|
1077
|
-
const updateInspectorIfOpen = () => {
|
|
1078
|
-
if (!inspectorEnabled) return;
|
|
1079
|
-
if (isInspectorOpen()) updateInspector();
|
|
1080
|
-
};
|
|
1081
|
-
|
|
1082
|
-
const applyThemeMode = (mode, { persist = true } = {}) => {
|
|
1083
|
-
themeMode = normalizeThemeMode(mode);
|
|
1084
|
-
if (persist) {
|
|
1085
|
-
try {
|
|
1086
|
-
localStorage.setItem(THEME_STORAGE_KEY, themeMode);
|
|
1087
|
-
} catch {
|
|
1088
|
-
// ignore
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
const nextTheme = resolveTheme();
|
|
1092
|
-
const prevTheme = currentTheme;
|
|
1093
|
-
currentTheme = nextTheme;
|
|
1094
|
-
document.documentElement.dataset.theme = nextTheme;
|
|
1095
|
-
document.documentElement.dataset.themeMode = themeMode;
|
|
1096
|
-
updateThemeControls();
|
|
1097
|
-
updateInspectorIfOpen();
|
|
1098
|
-
if (nextTheme !== prevTheme) emitThemeChange(nextTheme);
|
|
1099
|
-
};
|
|
1100
|
-
|
|
1101
|
-
if (systemQuery && typeof systemQuery.addEventListener === 'function') {
|
|
1102
|
-
systemQuery.addEventListener('change', () => {
|
|
1103
|
-
if (themeMode === 'system') applyThemeMode('system', { persist: false });
|
|
1104
|
-
});
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1242
|
+
|
|
1243
|
+
const updateInspectorIfOpen = () => {
|
|
1244
|
+
if (!inspectorEnabled) return;
|
|
1245
|
+
if (isInspectorOpen()) updateInspector();
|
|
1246
|
+
};
|
|
1247
|
+
|
|
1248
|
+
const applyThemeMode = (mode, { persist = true } = {}) => {
|
|
1249
|
+
themeMode = normalizeThemeMode(mode);
|
|
1250
|
+
if (persist) {
|
|
1251
|
+
try {
|
|
1252
|
+
localStorage.setItem(THEME_STORAGE_KEY, themeMode);
|
|
1253
|
+
} catch {
|
|
1254
|
+
// ignore
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
const nextTheme = resolveTheme();
|
|
1258
|
+
const prevTheme = currentTheme;
|
|
1259
|
+
currentTheme = nextTheme;
|
|
1260
|
+
document.documentElement.dataset.theme = nextTheme;
|
|
1261
|
+
document.documentElement.dataset.themeMode = themeMode;
|
|
1262
|
+
updateThemeControls();
|
|
1263
|
+
updateInspectorIfOpen();
|
|
1264
|
+
if (nextTheme !== prevTheme) emitThemeChange(nextTheme);
|
|
1265
|
+
};
|
|
1266
|
+
|
|
1267
|
+
if (systemQuery && typeof systemQuery.addEventListener === 'function') {
|
|
1268
|
+
systemQuery.addEventListener('change', () => {
|
|
1269
|
+
if (themeMode === 'system') applyThemeMode('system', { persist: false });
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1107
1273
|
if (btnThemeLight) btnThemeLight.addEventListener('click', () => applyThemeMode('light'));
|
|
1108
1274
|
if (btnThemeDark) btnThemeDark.addEventListener('click', () => applyThemeMode('dark'));
|
|
1109
1275
|
if (btnThemeSystem) btnThemeSystem.addEventListener('click', () => applyThemeMode('system'));
|
|
@@ -1112,156 +1278,167 @@ if (btnLlmClose) btnLlmClose.addEventListener('click', () => setLlmPanelOpen(fal
|
|
|
1112
1278
|
if (btnLlmRefresh) btnLlmRefresh.addEventListener('click', () => refreshLlmConfig());
|
|
1113
1279
|
if (btnLlmSave) btnLlmSave.addEventListener('click', () => saveLlmConfig());
|
|
1114
1280
|
if (btnLlmClear) btnLlmClear.addEventListener('click', () => saveLlmConfig({ clearKey: true }));
|
|
1281
|
+
if (btnMcpTest) btnMcpTest.addEventListener('click', () => setMcpPanelOpen(!isMcpPanelOpen()));
|
|
1282
|
+
if (btnMcpClose) btnMcpClose.addEventListener('click', () => setMcpPanelOpen(false));
|
|
1283
|
+
if (btnMcpClear)
|
|
1284
|
+
btnMcpClear.addEventListener('click', () => {
|
|
1285
|
+
if (mcpOutput) mcpOutput.textContent = '';
|
|
1286
|
+
setMcpStatus('');
|
|
1287
|
+
});
|
|
1288
|
+
if (btnMcpSend)
|
|
1289
|
+
btnMcpSend.addEventListener('click', () => {
|
|
1290
|
+
runMcpTest();
|
|
1291
|
+
});
|
|
1115
1292
|
if (btnInspectorToggle) btnInspectorToggle.addEventListener('click', () => setInspectorOpen(!isInspectorOpen()));
|
|
1116
1293
|
if (btnInspectorClose) btnInspectorClose.addEventListener('click', () => setInspectorOpen(false));
|
|
1117
1294
|
if (btnInspectorRefresh) btnInspectorRefresh.addEventListener('click', () => updateInspector());
|
|
1118
|
-
|
|
1119
|
-
applyThemeMode(themeMode || 'system', { persist: false });
|
|
1120
|
-
updateContextStatus();
|
|
1121
|
-
|
|
1122
|
-
const entries = [];
|
|
1123
|
-
const listeners = new Set();
|
|
1124
|
-
const emitUpdate = () => {
|
|
1125
|
-
const payload = { path: '(sandbox)', entries: [...entries] };
|
|
1126
|
-
for (const fn of listeners) { try { fn(payload); } catch {} }
|
|
1127
|
-
renderPrompts();
|
|
1128
|
-
};
|
|
1129
|
-
|
|
1130
|
-
const uuid = () => (globalThis.crypto?.randomUUID ? crypto.randomUUID() : String(Date.now()) + '-' + Math.random().toString(16).slice(2));
|
|
1131
|
-
|
|
1132
|
-
function renderPrompts() {
|
|
1133
|
-
panelBody.textContent = '';
|
|
1134
|
-
const pending = new Map();
|
|
1135
|
-
for (const e of entries) {
|
|
1136
|
-
if (e?.type !== 'ui_prompt') continue;
|
|
1137
|
-
const id = String(e?.requestId || '');
|
|
1138
|
-
if (!id) continue;
|
|
1139
|
-
if (e.action === 'request') pending.set(id, e);
|
|
1140
|
-
if (e.action === 'response') pending.delete(id);
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
if (pending.size === 0) {
|
|
1144
|
-
const empty = document.createElement('div');
|
|
1145
|
-
empty.className = 'muted';
|
|
1146
|
-
empty.textContent = '暂无待办(request 后会出现在这里)';
|
|
1147
|
-
panelBody.appendChild(empty);
|
|
1148
|
-
return;
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
for (const [requestId, req] of pending.entries()) {
|
|
1152
|
-
const card = document.createElement('div');
|
|
1153
|
-
card.className = 'card';
|
|
1154
|
-
|
|
1155
|
-
const title = document.createElement('div');
|
|
1156
|
-
title.style.fontWeight = '800';
|
|
1157
|
-
title.textContent = req?.prompt?.title || '(untitled)';
|
|
1158
|
-
|
|
1159
|
-
const msg = document.createElement('div');
|
|
1160
|
-
msg.className = 'muted';
|
|
1161
|
-
msg.style.marginTop = '6px';
|
|
1162
|
-
msg.textContent = req?.prompt?.message || '';
|
|
1163
|
-
|
|
1164
|
-
const source = document.createElement('div');
|
|
1165
|
-
source.className = 'muted';
|
|
1166
|
-
source.style.marginTop = '6px';
|
|
1167
|
-
source.textContent = req?.prompt?.source ? String(req.prompt.source) : '';
|
|
1168
|
-
|
|
1169
|
-
const form = document.createElement('div');
|
|
1170
|
-
form.style.marginTop = '10px';
|
|
1171
|
-
form.style.display = 'grid';
|
|
1172
|
-
form.style.gap = '10px';
|
|
1173
|
-
|
|
1174
|
-
const kind = String(req?.prompt?.kind || '');
|
|
1175
|
-
|
|
1176
|
-
const mkBtn = (label, danger) => {
|
|
1177
|
-
const btn = document.createElement('button');
|
|
1178
|
-
btn.type = 'button';
|
|
1179
|
-
btn.className = 'btn' + (danger ? ' danger' : '');
|
|
1180
|
-
btn.textContent = label;
|
|
1181
|
-
return btn;
|
|
1182
|
-
};
|
|
1183
|
-
|
|
1184
|
-
const submit = async (response) => {
|
|
1185
|
-
entries.push({ ts: new Date().toISOString(), type: 'ui_prompt', action: 'response', requestId, response });
|
|
1186
|
-
emitUpdate();
|
|
1187
|
-
};
|
|
1188
|
-
|
|
1189
|
-
if (kind === 'kv') {
|
|
1190
|
-
const fields = Array.isArray(req?.prompt?.fields) ? req.prompt.fields : [];
|
|
1191
|
-
const values = {};
|
|
1192
|
-
for (const f of fields) {
|
|
1193
|
-
const key = String(f?.key || '');
|
|
1194
|
-
if (!key) continue;
|
|
1195
|
-
const wrap = document.createElement('div');
|
|
1196
|
-
const lab = document.createElement('label');
|
|
1197
|
-
lab.textContent = f?.label ? String(f.label) : key;
|
|
1198
|
-
const input = document.createElement(f?.multiline ? 'textarea' : 'input');
|
|
1199
|
-
input.placeholder = f?.placeholder ? String(f.placeholder) : '';
|
|
1200
|
-
input.value = f?.default ? String(f.default) : '';
|
|
1201
|
-
input.addEventListener('input', () => { values[key] = String(input.value || ''); });
|
|
1202
|
-
values[key] = String(input.value || '');
|
|
1203
|
-
wrap.appendChild(lab);
|
|
1204
|
-
wrap.appendChild(input);
|
|
1205
|
-
form.appendChild(wrap);
|
|
1206
|
-
}
|
|
1207
|
-
const row = document.createElement('div');
|
|
1208
|
-
row.className = 'row';
|
|
1209
|
-
const ok = mkBtn('Submit');
|
|
1210
|
-
ok.addEventListener('click', () => submit({ status: 'ok', values }));
|
|
1211
|
-
const cancel = mkBtn('Cancel', true);
|
|
1212
|
-
cancel.addEventListener('click', () => submit({ status: 'cancel' }));
|
|
1213
|
-
row.appendChild(ok);
|
|
1214
|
-
row.appendChild(cancel);
|
|
1215
|
-
form.appendChild(row);
|
|
1216
|
-
} else if (kind === 'choice') {
|
|
1217
|
-
const options = Array.isArray(req?.prompt?.options) ? req.prompt.options : [];
|
|
1218
|
-
const multiple = Boolean(req?.prompt?.multiple);
|
|
1219
|
-
const selected = new Set();
|
|
1220
|
-
const wrap = document.createElement('div');
|
|
1221
|
-
const lab = document.createElement('label');
|
|
1222
|
-
lab.textContent = '选择';
|
|
1223
|
-
const select = document.createElement('select');
|
|
1224
|
-
if (multiple) select.multiple = true;
|
|
1225
|
-
for (const opt of options) {
|
|
1226
|
-
const v = String(opt?.value || '');
|
|
1227
|
-
const o = document.createElement('option');
|
|
1228
|
-
o.value = v;
|
|
1229
|
-
o.textContent = opt?.label ? String(opt.label) : v;
|
|
1230
|
-
select.appendChild(o);
|
|
1231
|
-
}
|
|
1232
|
-
select.addEventListener('change', () => {
|
|
1233
|
-
selected.clear();
|
|
1234
|
-
for (const o of select.selectedOptions) selected.add(String(o.value));
|
|
1235
|
-
});
|
|
1236
|
-
wrap.appendChild(lab);
|
|
1237
|
-
wrap.appendChild(select);
|
|
1238
|
-
form.appendChild(wrap);
|
|
1239
|
-
const row = document.createElement('div');
|
|
1240
|
-
row.className = 'row';
|
|
1241
|
-
const ok = mkBtn('Submit');
|
|
1242
|
-
ok.addEventListener('click', () => submit({ status: 'ok', value: multiple ? Array.from(selected) : Array.from(selected)[0] || '' }));
|
|
1243
|
-
const cancel = mkBtn('Cancel', true);
|
|
1244
|
-
cancel.addEventListener('click', () => submit({ status: 'cancel' }));
|
|
1245
|
-
row.appendChild(ok);
|
|
1246
|
-
row.appendChild(cancel);
|
|
1247
|
-
form.appendChild(row);
|
|
1248
|
-
} else {
|
|
1249
|
-
const row = document.createElement('div');
|
|
1250
|
-
row.className = 'row';
|
|
1251
|
-
const ok = mkBtn('OK');
|
|
1252
|
-
ok.addEventListener('click', () => submit({ status: 'ok' }));
|
|
1253
|
-
const cancel = mkBtn('Cancel', true);
|
|
1254
|
-
cancel.addEventListener('click', () => submit({ status: 'cancel' }));
|
|
1255
|
-
row.appendChild(ok);
|
|
1256
|
-
row.appendChild(cancel);
|
|
1257
|
-
form.appendChild(row);
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
card.appendChild(title);
|
|
1261
|
-
if (msg.textContent) card.appendChild(msg);
|
|
1262
|
-
if (source.textContent) card.appendChild(source);
|
|
1263
|
-
card.appendChild(form);
|
|
1264
|
-
panelBody.appendChild(card);
|
|
1295
|
+
|
|
1296
|
+
applyThemeMode(themeMode || 'system', { persist: false });
|
|
1297
|
+
updateContextStatus();
|
|
1298
|
+
|
|
1299
|
+
const entries = [];
|
|
1300
|
+
const listeners = new Set();
|
|
1301
|
+
const emitUpdate = () => {
|
|
1302
|
+
const payload = { path: '(sandbox)', entries: [...entries] };
|
|
1303
|
+
for (const fn of listeners) { try { fn(payload); } catch {} }
|
|
1304
|
+
renderPrompts();
|
|
1305
|
+
};
|
|
1306
|
+
|
|
1307
|
+
const uuid = () => (globalThis.crypto?.randomUUID ? crypto.randomUUID() : String(Date.now()) + '-' + Math.random().toString(16).slice(2));
|
|
1308
|
+
|
|
1309
|
+
function renderPrompts() {
|
|
1310
|
+
panelBody.textContent = '';
|
|
1311
|
+
const pending = new Map();
|
|
1312
|
+
for (const e of entries) {
|
|
1313
|
+
if (e?.type !== 'ui_prompt') continue;
|
|
1314
|
+
const id = String(e?.requestId || '');
|
|
1315
|
+
if (!id) continue;
|
|
1316
|
+
if (e.action === 'request') pending.set(id, e);
|
|
1317
|
+
if (e.action === 'response') pending.delete(id);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
if (pending.size === 0) {
|
|
1321
|
+
const empty = document.createElement('div');
|
|
1322
|
+
empty.className = 'muted';
|
|
1323
|
+
empty.textContent = '暂无待办(request 后会出现在这里)';
|
|
1324
|
+
panelBody.appendChild(empty);
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
for (const [requestId, req] of pending.entries()) {
|
|
1329
|
+
const card = document.createElement('div');
|
|
1330
|
+
card.className = 'card';
|
|
1331
|
+
|
|
1332
|
+
const title = document.createElement('div');
|
|
1333
|
+
title.style.fontWeight = '800';
|
|
1334
|
+
title.textContent = req?.prompt?.title || '(untitled)';
|
|
1335
|
+
|
|
1336
|
+
const msg = document.createElement('div');
|
|
1337
|
+
msg.className = 'muted';
|
|
1338
|
+
msg.style.marginTop = '6px';
|
|
1339
|
+
msg.textContent = req?.prompt?.message || '';
|
|
1340
|
+
|
|
1341
|
+
const source = document.createElement('div');
|
|
1342
|
+
source.className = 'muted';
|
|
1343
|
+
source.style.marginTop = '6px';
|
|
1344
|
+
source.textContent = req?.prompt?.source ? String(req.prompt.source) : '';
|
|
1345
|
+
|
|
1346
|
+
const form = document.createElement('div');
|
|
1347
|
+
form.style.marginTop = '10px';
|
|
1348
|
+
form.style.display = 'grid';
|
|
1349
|
+
form.style.gap = '10px';
|
|
1350
|
+
|
|
1351
|
+
const kind = String(req?.prompt?.kind || '');
|
|
1352
|
+
|
|
1353
|
+
const mkBtn = (label, danger) => {
|
|
1354
|
+
const btn = document.createElement('button');
|
|
1355
|
+
btn.type = 'button';
|
|
1356
|
+
btn.className = 'btn' + (danger ? ' danger' : '');
|
|
1357
|
+
btn.textContent = label;
|
|
1358
|
+
return btn;
|
|
1359
|
+
};
|
|
1360
|
+
|
|
1361
|
+
const submit = async (response) => {
|
|
1362
|
+
entries.push({ ts: new Date().toISOString(), type: 'ui_prompt', action: 'response', requestId, response });
|
|
1363
|
+
emitUpdate();
|
|
1364
|
+
};
|
|
1365
|
+
|
|
1366
|
+
if (kind === 'kv') {
|
|
1367
|
+
const fields = Array.isArray(req?.prompt?.fields) ? req.prompt.fields : [];
|
|
1368
|
+
const values = {};
|
|
1369
|
+
for (const f of fields) {
|
|
1370
|
+
const key = String(f?.key || '');
|
|
1371
|
+
if (!key) continue;
|
|
1372
|
+
const wrap = document.createElement('div');
|
|
1373
|
+
const lab = document.createElement('label');
|
|
1374
|
+
lab.textContent = f?.label ? String(f.label) : key;
|
|
1375
|
+
const input = document.createElement(f?.multiline ? 'textarea' : 'input');
|
|
1376
|
+
input.placeholder = f?.placeholder ? String(f.placeholder) : '';
|
|
1377
|
+
input.value = f?.default ? String(f.default) : '';
|
|
1378
|
+
input.addEventListener('input', () => { values[key] = String(input.value || ''); });
|
|
1379
|
+
values[key] = String(input.value || '');
|
|
1380
|
+
wrap.appendChild(lab);
|
|
1381
|
+
wrap.appendChild(input);
|
|
1382
|
+
form.appendChild(wrap);
|
|
1383
|
+
}
|
|
1384
|
+
const row = document.createElement('div');
|
|
1385
|
+
row.className = 'row';
|
|
1386
|
+
const ok = mkBtn('Submit');
|
|
1387
|
+
ok.addEventListener('click', () => submit({ status: 'ok', values }));
|
|
1388
|
+
const cancel = mkBtn('Cancel', true);
|
|
1389
|
+
cancel.addEventListener('click', () => submit({ status: 'cancel' }));
|
|
1390
|
+
row.appendChild(ok);
|
|
1391
|
+
row.appendChild(cancel);
|
|
1392
|
+
form.appendChild(row);
|
|
1393
|
+
} else if (kind === 'choice') {
|
|
1394
|
+
const options = Array.isArray(req?.prompt?.options) ? req.prompt.options : [];
|
|
1395
|
+
const multiple = Boolean(req?.prompt?.multiple);
|
|
1396
|
+
const selected = new Set();
|
|
1397
|
+
const wrap = document.createElement('div');
|
|
1398
|
+
const lab = document.createElement('label');
|
|
1399
|
+
lab.textContent = '选择';
|
|
1400
|
+
const select = document.createElement('select');
|
|
1401
|
+
if (multiple) select.multiple = true;
|
|
1402
|
+
for (const opt of options) {
|
|
1403
|
+
const v = String(opt?.value || '');
|
|
1404
|
+
const o = document.createElement('option');
|
|
1405
|
+
o.value = v;
|
|
1406
|
+
o.textContent = opt?.label ? String(opt.label) : v;
|
|
1407
|
+
select.appendChild(o);
|
|
1408
|
+
}
|
|
1409
|
+
select.addEventListener('change', () => {
|
|
1410
|
+
selected.clear();
|
|
1411
|
+
for (const o of select.selectedOptions) selected.add(String(o.value));
|
|
1412
|
+
});
|
|
1413
|
+
wrap.appendChild(lab);
|
|
1414
|
+
wrap.appendChild(select);
|
|
1415
|
+
form.appendChild(wrap);
|
|
1416
|
+
const row = document.createElement('div');
|
|
1417
|
+
row.className = 'row';
|
|
1418
|
+
const ok = mkBtn('Submit');
|
|
1419
|
+
ok.addEventListener('click', () => submit({ status: 'ok', value: multiple ? Array.from(selected) : Array.from(selected)[0] || '' }));
|
|
1420
|
+
const cancel = mkBtn('Cancel', true);
|
|
1421
|
+
cancel.addEventListener('click', () => submit({ status: 'cancel' }));
|
|
1422
|
+
row.appendChild(ok);
|
|
1423
|
+
row.appendChild(cancel);
|
|
1424
|
+
form.appendChild(row);
|
|
1425
|
+
} else {
|
|
1426
|
+
const row = document.createElement('div');
|
|
1427
|
+
row.className = 'row';
|
|
1428
|
+
const ok = mkBtn('OK');
|
|
1429
|
+
ok.addEventListener('click', () => submit({ status: 'ok' }));
|
|
1430
|
+
const cancel = mkBtn('Cancel', true);
|
|
1431
|
+
cancel.addEventListener('click', () => submit({ status: 'cancel' }));
|
|
1432
|
+
row.appendChild(ok);
|
|
1433
|
+
row.appendChild(cancel);
|
|
1434
|
+
form.appendChild(row);
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
card.appendChild(title);
|
|
1438
|
+
if (msg.textContent) card.appendChild(msg);
|
|
1439
|
+
if (source.textContent) card.appendChild(source);
|
|
1440
|
+
card.appendChild(form);
|
|
1441
|
+
panelBody.appendChild(card);
|
|
1265
1442
|
}
|
|
1266
1443
|
}
|
|
1267
1444
|
|
|
@@ -1291,120 +1468,120 @@ const callSandboxChat = async (payload, signal) => {
|
|
|
1291
1468
|
};
|
|
1292
1469
|
|
|
1293
1470
|
const getTheme = () => currentTheme || resolveTheme();
|
|
1294
|
-
|
|
1471
|
+
|
|
1295
1472
|
const host = {
|
|
1296
1473
|
bridge: { enabled: true },
|
|
1297
|
-
context: { get: () => ({
|
|
1474
|
+
context: { get: () => ({ pluginId: __SANDBOX__.pluginId, appId: __SANDBOX__.appId, theme: getTheme(), bridge: { enabled: true } }) },
|
|
1298
1475
|
theme: {
|
|
1299
|
-
get: getTheme,
|
|
1300
|
-
onChange: (listener) => {
|
|
1301
|
-
if (typeof listener !== 'function') return () => {};
|
|
1302
|
-
themeListeners.add(listener);
|
|
1303
|
-
return () => themeListeners.delete(listener);
|
|
1304
|
-
},
|
|
1305
|
-
},
|
|
1306
|
-
admin: {
|
|
1307
|
-
state: async () => ({ ok: true, state: {} }),
|
|
1308
|
-
onUpdate: () => () => {},
|
|
1309
|
-
models: { list: async () => ({ ok: true, models: [] }) },
|
|
1310
|
-
secrets: { list: async () => ({ ok: true, secrets: [] }) },
|
|
1311
|
-
},
|
|
1312
|
-
registry: {
|
|
1313
|
-
list: async () => ({ ok: true, apps: [__SANDBOX__.registryApp] }),
|
|
1314
|
-
},
|
|
1315
|
-
backend: {
|
|
1316
|
-
invoke: async (method, params) => {
|
|
1317
|
-
const r = await fetch('/api/backend/invoke', {
|
|
1318
|
-
method: 'POST',
|
|
1319
|
-
headers: { 'content-type': 'application/json' },
|
|
1320
|
-
body: JSON.stringify({ method, params }),
|
|
1321
|
-
});
|
|
1322
|
-
const j = await r.json();
|
|
1323
|
-
if (j?.ok === false) throw new Error(j?.message || 'invoke failed');
|
|
1324
|
-
return j?.result;
|
|
1325
|
-
},
|
|
1326
|
-
},
|
|
1327
|
-
uiPrompts: {
|
|
1328
|
-
read: async () => ({ path: '(sandbox)', entries: [...entries] }),
|
|
1329
|
-
onUpdate: (listener) => { listeners.add(listener); return () => listeners.delete(listener); },
|
|
1330
|
-
request: async (payload) => {
|
|
1331
|
-
const requestId = payload?.requestId ? String(payload.requestId) : uuid();
|
|
1332
|
-
const prompt = payload?.prompt && typeof payload.prompt === 'object' ? { ...payload.prompt } : null;
|
|
1333
|
-
if (prompt && !prompt.source) prompt.source = __SANDBOX__.pluginId + ':' + __SANDBOX__.appId;
|
|
1334
|
-
entries.push({ ts: new Date().toISOString(), type: 'ui_prompt', action: 'request', requestId, runId: payload?.runId, prompt });
|
|
1335
|
-
emitUpdate();
|
|
1336
|
-
return { ok: true, requestId };
|
|
1337
|
-
},
|
|
1338
|
-
respond: async (payload) => {
|
|
1339
|
-
const requestId = String(payload?.requestId || '');
|
|
1340
|
-
if (!requestId) throw new Error('requestId is required');
|
|
1341
|
-
const response = payload?.response && typeof payload.response === 'object' ? payload.response : null;
|
|
1342
|
-
entries.push({ ts: new Date().toISOString(), type: 'ui_prompt', action: 'response', requestId, runId: payload?.runId, response });
|
|
1343
|
-
emitUpdate();
|
|
1344
|
-
return { ok: true };
|
|
1345
|
-
},
|
|
1346
|
-
open: () => (setPanelOpen(true), { ok: true }),
|
|
1347
|
-
close: () => (setPanelOpen(false), { ok: true }),
|
|
1348
|
-
toggle: () => (setPanelOpen(panel.style.display !== 'flex'), { ok: true }),
|
|
1349
|
-
},
|
|
1350
|
-
ui: { navigate: (menu) => ({ ok: true, menu }) },
|
|
1351
|
-
chat: (() => {
|
|
1352
|
-
const clone = (v) => JSON.parse(JSON.stringify(v));
|
|
1353
|
-
|
|
1354
|
-
const agents = [
|
|
1355
|
-
{
|
|
1356
|
-
id: 'sandbox-agent',
|
|
1357
|
-
name: 'Sandbox Agent',
|
|
1358
|
-
description: 'Mock agent for ChatOS UI Apps Sandbox',
|
|
1359
|
-
},
|
|
1360
|
-
];
|
|
1361
|
-
|
|
1362
|
-
const sessions = new Map();
|
|
1363
|
-
const defaultSessionByAgent = new Map();
|
|
1364
|
-
const messagesBySession = new Map();
|
|
1365
|
-
|
|
1366
|
-
const listeners = new Set();
|
|
1367
|
-
const activeRuns = new Map(); // sessionId -> { aborted: boolean, timers: number[] }
|
|
1368
|
-
|
|
1369
|
-
const emit = (payload) => {
|
|
1370
|
-
for (const sub of listeners) {
|
|
1371
|
-
const filter = sub?.filter && typeof sub.filter === 'object' ? sub.filter : {};
|
|
1372
|
-
if (filter?.sessionId && String(filter.sessionId) !== String(payload?.sessionId || '')) continue;
|
|
1373
|
-
if (Array.isArray(filter?.types) && filter.types.length > 0) {
|
|
1374
|
-
const t = String(payload?.type || '');
|
|
1375
|
-
if (!filter.types.includes(t)) continue;
|
|
1376
|
-
}
|
|
1377
|
-
try {
|
|
1378
|
-
sub.fn(payload);
|
|
1379
|
-
} catch {
|
|
1380
|
-
// ignore
|
|
1381
|
-
}
|
|
1382
|
-
}
|
|
1383
|
-
};
|
|
1384
|
-
|
|
1385
|
-
const ensureAgent = async () => {
|
|
1386
|
-
if (agents.length > 0) return agents[0];
|
|
1387
|
-
const created = { id: 'sandbox-agent', name: 'Sandbox Agent', description: 'Mock agent' };
|
|
1388
|
-
agents.push(created);
|
|
1389
|
-
return created;
|
|
1390
|
-
};
|
|
1391
|
-
|
|
1392
|
-
const ensureSession = async (agentId) => {
|
|
1393
|
-
const aid = String(agentId || '').trim() || (await ensureAgent()).id;
|
|
1394
|
-
const existingId = defaultSessionByAgent.get(aid);
|
|
1395
|
-
if (existingId && sessions.has(existingId)) return sessions.get(existingId);
|
|
1396
|
-
|
|
1397
|
-
const id = 'sandbox-session-' + uuid();
|
|
1398
|
-
const session = { id, agentId: aid, createdAt: new Date().toISOString() };
|
|
1399
|
-
sessions.set(id, session);
|
|
1400
|
-
defaultSessionByAgent.set(aid, id);
|
|
1401
|
-
if (!messagesBySession.has(id)) messagesBySession.set(id, []);
|
|
1402
|
-
return session;
|
|
1403
|
-
};
|
|
1404
|
-
|
|
1405
|
-
const agentsApi = {
|
|
1406
|
-
list: async () => ({ ok: true, agents: clone(agents) }),
|
|
1407
|
-
ensureDefault: async () => ({ ok: true, agent: clone(await ensureAgent()) }),
|
|
1476
|
+
get: getTheme,
|
|
1477
|
+
onChange: (listener) => {
|
|
1478
|
+
if (typeof listener !== 'function') return () => {};
|
|
1479
|
+
themeListeners.add(listener);
|
|
1480
|
+
return () => themeListeners.delete(listener);
|
|
1481
|
+
},
|
|
1482
|
+
},
|
|
1483
|
+
admin: {
|
|
1484
|
+
state: async () => ({ ok: true, state: {} }),
|
|
1485
|
+
onUpdate: () => () => {},
|
|
1486
|
+
models: { list: async () => ({ ok: true, models: [] }) },
|
|
1487
|
+
secrets: { list: async () => ({ ok: true, secrets: [] }) },
|
|
1488
|
+
},
|
|
1489
|
+
registry: {
|
|
1490
|
+
list: async () => ({ ok: true, apps: [__SANDBOX__.registryApp] }),
|
|
1491
|
+
},
|
|
1492
|
+
backend: {
|
|
1493
|
+
invoke: async (method, params) => {
|
|
1494
|
+
const r = await fetch('/api/backend/invoke', {
|
|
1495
|
+
method: 'POST',
|
|
1496
|
+
headers: { 'content-type': 'application/json' },
|
|
1497
|
+
body: JSON.stringify({ method, params }),
|
|
1498
|
+
});
|
|
1499
|
+
const j = await r.json();
|
|
1500
|
+
if (j?.ok === false) throw new Error(j?.message || 'invoke failed');
|
|
1501
|
+
return j?.result;
|
|
1502
|
+
},
|
|
1503
|
+
},
|
|
1504
|
+
uiPrompts: {
|
|
1505
|
+
read: async () => ({ path: '(sandbox)', entries: [...entries] }),
|
|
1506
|
+
onUpdate: (listener) => { listeners.add(listener); return () => listeners.delete(listener); },
|
|
1507
|
+
request: async (payload) => {
|
|
1508
|
+
const requestId = payload?.requestId ? String(payload.requestId) : uuid();
|
|
1509
|
+
const prompt = payload?.prompt && typeof payload.prompt === 'object' ? { ...payload.prompt } : null;
|
|
1510
|
+
if (prompt && !prompt.source) prompt.source = __SANDBOX__.pluginId + ':' + __SANDBOX__.appId;
|
|
1511
|
+
entries.push({ ts: new Date().toISOString(), type: 'ui_prompt', action: 'request', requestId, runId: payload?.runId, prompt });
|
|
1512
|
+
emitUpdate();
|
|
1513
|
+
return { ok: true, requestId };
|
|
1514
|
+
},
|
|
1515
|
+
respond: async (payload) => {
|
|
1516
|
+
const requestId = String(payload?.requestId || '');
|
|
1517
|
+
if (!requestId) throw new Error('requestId is required');
|
|
1518
|
+
const response = payload?.response && typeof payload.response === 'object' ? payload.response : null;
|
|
1519
|
+
entries.push({ ts: new Date().toISOString(), type: 'ui_prompt', action: 'response', requestId, runId: payload?.runId, response });
|
|
1520
|
+
emitUpdate();
|
|
1521
|
+
return { ok: true };
|
|
1522
|
+
},
|
|
1523
|
+
open: () => (setPanelOpen(true), { ok: true }),
|
|
1524
|
+
close: () => (setPanelOpen(false), { ok: true }),
|
|
1525
|
+
toggle: () => (setPanelOpen(panel.style.display !== 'flex'), { ok: true }),
|
|
1526
|
+
},
|
|
1527
|
+
ui: { navigate: (menu) => ({ ok: true, menu }) },
|
|
1528
|
+
chat: (() => {
|
|
1529
|
+
const clone = (v) => JSON.parse(JSON.stringify(v));
|
|
1530
|
+
|
|
1531
|
+
const agents = [
|
|
1532
|
+
{
|
|
1533
|
+
id: 'sandbox-agent',
|
|
1534
|
+
name: 'Sandbox Agent',
|
|
1535
|
+
description: 'Mock agent for ChatOS UI Apps Sandbox',
|
|
1536
|
+
},
|
|
1537
|
+
];
|
|
1538
|
+
|
|
1539
|
+
const sessions = new Map();
|
|
1540
|
+
const defaultSessionByAgent = new Map();
|
|
1541
|
+
const messagesBySession = new Map();
|
|
1542
|
+
|
|
1543
|
+
const listeners = new Set();
|
|
1544
|
+
const activeRuns = new Map(); // sessionId -> { aborted: boolean, timers: number[] }
|
|
1545
|
+
|
|
1546
|
+
const emit = (payload) => {
|
|
1547
|
+
for (const sub of listeners) {
|
|
1548
|
+
const filter = sub?.filter && typeof sub.filter === 'object' ? sub.filter : {};
|
|
1549
|
+
if (filter?.sessionId && String(filter.sessionId) !== String(payload?.sessionId || '')) continue;
|
|
1550
|
+
if (Array.isArray(filter?.types) && filter.types.length > 0) {
|
|
1551
|
+
const t = String(payload?.type || '');
|
|
1552
|
+
if (!filter.types.includes(t)) continue;
|
|
1553
|
+
}
|
|
1554
|
+
try {
|
|
1555
|
+
sub.fn(payload);
|
|
1556
|
+
} catch {
|
|
1557
|
+
// ignore
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
};
|
|
1561
|
+
|
|
1562
|
+
const ensureAgent = async () => {
|
|
1563
|
+
if (agents.length > 0) return agents[0];
|
|
1564
|
+
const created = { id: 'sandbox-agent', name: 'Sandbox Agent', description: 'Mock agent' };
|
|
1565
|
+
agents.push(created);
|
|
1566
|
+
return created;
|
|
1567
|
+
};
|
|
1568
|
+
|
|
1569
|
+
const ensureSession = async (agentId) => {
|
|
1570
|
+
const aid = String(agentId || '').trim() || (await ensureAgent()).id;
|
|
1571
|
+
const existingId = defaultSessionByAgent.get(aid);
|
|
1572
|
+
if (existingId && sessions.has(existingId)) return sessions.get(existingId);
|
|
1573
|
+
|
|
1574
|
+
const id = 'sandbox-session-' + uuid();
|
|
1575
|
+
const session = { id, agentId: aid, createdAt: new Date().toISOString() };
|
|
1576
|
+
sessions.set(id, session);
|
|
1577
|
+
defaultSessionByAgent.set(aid, id);
|
|
1578
|
+
if (!messagesBySession.has(id)) messagesBySession.set(id, []);
|
|
1579
|
+
return session;
|
|
1580
|
+
};
|
|
1581
|
+
|
|
1582
|
+
const agentsApi = {
|
|
1583
|
+
list: async () => ({ ok: true, agents: clone(agents) }),
|
|
1584
|
+
ensureDefault: async () => ({ ok: true, agent: clone(await ensureAgent()) }),
|
|
1408
1585
|
create: async (payload) => {
|
|
1409
1586
|
const agent = {
|
|
1410
1587
|
id: 'sandbox-agent-' + uuid(),
|
|
@@ -1416,8 +1593,8 @@ const host = {
|
|
|
1416
1593
|
return { ok: true, agent: clone(agent) };
|
|
1417
1594
|
},
|
|
1418
1595
|
update: async (id, patch) => {
|
|
1419
|
-
const agentId = String(id || '').trim();
|
|
1420
|
-
if (!agentId) throw new Error('id is required');
|
|
1596
|
+
const agentId = String(id || '').trim();
|
|
1597
|
+
if (!agentId) throw new Error('id is required');
|
|
1421
1598
|
const idx = agents.findIndex((a) => a.id === agentId);
|
|
1422
1599
|
if (idx < 0) throw new Error('agent not found');
|
|
1423
1600
|
const a = agents[idx];
|
|
@@ -1426,48 +1603,48 @@ const host = {
|
|
|
1426
1603
|
if (patch?.modelId) a.modelId = String(patch.modelId);
|
|
1427
1604
|
return { ok: true, agent: clone(a) };
|
|
1428
1605
|
},
|
|
1429
|
-
delete: async (id) => {
|
|
1430
|
-
const agentId = String(id || '').trim();
|
|
1431
|
-
if (!agentId) throw new Error('id is required');
|
|
1432
|
-
const idx = agents.findIndex((a) => a.id === agentId);
|
|
1433
|
-
if (idx < 0) return { ok: true, deleted: false };
|
|
1434
|
-
agents.splice(idx, 1);
|
|
1435
|
-
return { ok: true, deleted: true };
|
|
1436
|
-
},
|
|
1437
|
-
createForApp: async (payload) => {
|
|
1438
|
-
const name = payload?.name ? String(payload.name) : 'App Agent (' + __SANDBOX__.appId + ')';
|
|
1439
|
-
return await agentsApi.create({ ...payload, name });
|
|
1440
|
-
},
|
|
1441
|
-
};
|
|
1442
|
-
|
|
1443
|
-
const sessionsApi = {
|
|
1444
|
-
list: async () => ({ ok: true, sessions: clone(Array.from(sessions.values())) }),
|
|
1445
|
-
ensureDefault: async (payload) => {
|
|
1446
|
-
const session = await ensureSession(payload?.agentId);
|
|
1447
|
-
return { ok: true, session: clone(session) };
|
|
1448
|
-
},
|
|
1449
|
-
create: async (payload) => {
|
|
1450
|
-
const agentId = payload?.agentId ? String(payload.agentId) : (await ensureAgent()).id;
|
|
1451
|
-
const id = 'sandbox-session-' + uuid();
|
|
1452
|
-
const session = { id, agentId, createdAt: new Date().toISOString() };
|
|
1453
|
-
sessions.set(id, session);
|
|
1454
|
-
if (!messagesBySession.has(id)) messagesBySession.set(id, []);
|
|
1455
|
-
return { ok: true, session: clone(session) };
|
|
1456
|
-
},
|
|
1457
|
-
};
|
|
1458
|
-
|
|
1459
|
-
const messagesApi = {
|
|
1460
|
-
list: async (payload) => {
|
|
1461
|
-
const sessionId = String(payload?.sessionId || '').trim();
|
|
1462
|
-
if (!sessionId) throw new Error('sessionId is required');
|
|
1463
|
-
const msgs = messagesBySession.get(sessionId) || [];
|
|
1464
|
-
return { ok: true, messages: clone(msgs) };
|
|
1465
|
-
},
|
|
1466
|
-
};
|
|
1467
|
-
|
|
1468
|
-
const abort = async (payload) => {
|
|
1469
|
-
const sessionId = String(payload?.sessionId || '').trim();
|
|
1470
|
-
if (!sessionId) throw new Error('sessionId is required');
|
|
1606
|
+
delete: async (id) => {
|
|
1607
|
+
const agentId = String(id || '').trim();
|
|
1608
|
+
if (!agentId) throw new Error('id is required');
|
|
1609
|
+
const idx = agents.findIndex((a) => a.id === agentId);
|
|
1610
|
+
if (idx < 0) return { ok: true, deleted: false };
|
|
1611
|
+
agents.splice(idx, 1);
|
|
1612
|
+
return { ok: true, deleted: true };
|
|
1613
|
+
},
|
|
1614
|
+
createForApp: async (payload) => {
|
|
1615
|
+
const name = payload?.name ? String(payload.name) : 'App Agent (' + __SANDBOX__.appId + ')';
|
|
1616
|
+
return await agentsApi.create({ ...payload, name });
|
|
1617
|
+
},
|
|
1618
|
+
};
|
|
1619
|
+
|
|
1620
|
+
const sessionsApi = {
|
|
1621
|
+
list: async () => ({ ok: true, sessions: clone(Array.from(sessions.values())) }),
|
|
1622
|
+
ensureDefault: async (payload) => {
|
|
1623
|
+
const session = await ensureSession(payload?.agentId);
|
|
1624
|
+
return { ok: true, session: clone(session) };
|
|
1625
|
+
},
|
|
1626
|
+
create: async (payload) => {
|
|
1627
|
+
const agentId = payload?.agentId ? String(payload.agentId) : (await ensureAgent()).id;
|
|
1628
|
+
const id = 'sandbox-session-' + uuid();
|
|
1629
|
+
const session = { id, agentId, createdAt: new Date().toISOString() };
|
|
1630
|
+
sessions.set(id, session);
|
|
1631
|
+
if (!messagesBySession.has(id)) messagesBySession.set(id, []);
|
|
1632
|
+
return { ok: true, session: clone(session) };
|
|
1633
|
+
},
|
|
1634
|
+
};
|
|
1635
|
+
|
|
1636
|
+
const messagesApi = {
|
|
1637
|
+
list: async (payload) => {
|
|
1638
|
+
const sessionId = String(payload?.sessionId || '').trim();
|
|
1639
|
+
if (!sessionId) throw new Error('sessionId is required');
|
|
1640
|
+
const msgs = messagesBySession.get(sessionId) || [];
|
|
1641
|
+
return { ok: true, messages: clone(msgs) };
|
|
1642
|
+
},
|
|
1643
|
+
};
|
|
1644
|
+
|
|
1645
|
+
const abort = async (payload) => {
|
|
1646
|
+
const sessionId = String(payload?.sessionId || '').trim();
|
|
1647
|
+
if (!sessionId) throw new Error('sessionId is required');
|
|
1471
1648
|
const run = activeRuns.get(sessionId);
|
|
1472
1649
|
if (run) {
|
|
1473
1650
|
run.aborted = true;
|
|
@@ -1483,28 +1660,28 @@ const host = {
|
|
|
1483
1660
|
clearTimeout(t);
|
|
1484
1661
|
} catch {
|
|
1485
1662
|
// ignore
|
|
1486
|
-
}
|
|
1487
|
-
}
|
|
1488
|
-
activeRuns.delete(sessionId);
|
|
1489
|
-
}
|
|
1490
|
-
emit({ type: 'assistant_abort', sessionId, ts: new Date().toISOString() });
|
|
1491
|
-
return { ok: true };
|
|
1492
|
-
};
|
|
1493
|
-
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
activeRuns.delete(sessionId);
|
|
1666
|
+
}
|
|
1667
|
+
emit({ type: 'assistant_abort', sessionId, ts: new Date().toISOString() });
|
|
1668
|
+
return { ok: true };
|
|
1669
|
+
};
|
|
1670
|
+
|
|
1494
1671
|
const send = async (payload) => {
|
|
1495
1672
|
const sessionId = String(payload?.sessionId || '').trim();
|
|
1496
1673
|
const text = String(payload?.text || '').trim();
|
|
1497
1674
|
if (!sessionId) throw new Error('sessionId is required');
|
|
1498
1675
|
if (!text) throw new Error('text is required');
|
|
1499
|
-
|
|
1500
|
-
if (!sessions.has(sessionId)) throw new Error('session not found');
|
|
1501
|
-
|
|
1502
|
-
const msgs = messagesBySession.get(sessionId) || [];
|
|
1503
|
-
const userMsg = { id: 'msg-' + uuid(), role: 'user', text, ts: new Date().toISOString() };
|
|
1504
|
-
msgs.push(userMsg);
|
|
1505
|
-
messagesBySession.set(sessionId, msgs);
|
|
1506
|
-
emit({ type: 'user_message', sessionId, message: clone(userMsg) });
|
|
1507
|
-
|
|
1676
|
+
|
|
1677
|
+
if (!sessions.has(sessionId)) throw new Error('session not found');
|
|
1678
|
+
|
|
1679
|
+
const msgs = messagesBySession.get(sessionId) || [];
|
|
1680
|
+
const userMsg = { id: 'msg-' + uuid(), role: 'user', text, ts: new Date().toISOString() };
|
|
1681
|
+
msgs.push(userMsg);
|
|
1682
|
+
messagesBySession.set(sessionId, msgs);
|
|
1683
|
+
emit({ type: 'user_message', sessionId, message: clone(userMsg) });
|
|
1684
|
+
|
|
1508
1685
|
const assistantMsg = { id: 'msg-' + uuid(), role: 'assistant', text: '', ts: new Date().toISOString() };
|
|
1509
1686
|
msgs.push(assistantMsg);
|
|
1510
1687
|
emit({ type: 'assistant_start', sessionId, message: clone(assistantMsg) });
|
|
@@ -1580,100 +1757,100 @@ const host = {
|
|
|
1580
1757
|
|
|
1581
1758
|
return { ok: true };
|
|
1582
1759
|
};
|
|
1583
|
-
|
|
1584
|
-
const events = {
|
|
1585
|
-
subscribe: (filter, fn) => {
|
|
1586
|
-
if (typeof fn !== 'function') throw new Error('listener is required');
|
|
1587
|
-
const sub = { filter: filter && typeof filter === 'object' ? { ...filter } : {}, fn };
|
|
1588
|
-
listeners.add(sub);
|
|
1589
|
-
return () => listeners.delete(sub);
|
|
1590
|
-
},
|
|
1591
|
-
unsubscribe: () => (listeners.clear(), { ok: true }),
|
|
1592
|
-
};
|
|
1593
|
-
|
|
1594
|
-
return {
|
|
1595
|
-
agents: agentsApi,
|
|
1596
|
-
sessions: sessionsApi,
|
|
1597
|
-
messages: messagesApi,
|
|
1598
|
-
send,
|
|
1599
|
-
abort,
|
|
1600
|
-
events,
|
|
1601
|
-
};
|
|
1602
|
-
})(),
|
|
1603
|
-
};
|
|
1604
|
-
|
|
1605
|
-
inspectorEnabled = true;
|
|
1606
|
-
updateInspector();
|
|
1607
|
-
|
|
1608
|
-
let dispose = null;
|
|
1609
|
-
|
|
1610
|
-
async function loadAndMount() {
|
|
1611
|
-
if (typeof dispose === 'function') { try { await dispose(); } catch {} dispose = null; }
|
|
1612
|
-
container.textContent = '';
|
|
1613
|
-
|
|
1614
|
-
const entryUrl = __SANDBOX__.entryUrl;
|
|
1615
|
-
const mod = await import(entryUrl + (entryUrl.includes('?') ? '&' : '?') + 't=' + Date.now());
|
|
1616
|
-
const mount = mod?.mount || mod?.default?.mount || (typeof mod?.default === 'function' ? mod.default : null);
|
|
1617
|
-
if (typeof mount !== 'function') throw new Error('module entry must export mount()');
|
|
1618
|
-
const ret = await mount({ container, host, slots: { header: headerSlot } });
|
|
1619
|
-
if (typeof ret === 'function') dispose = ret;
|
|
1620
|
-
else if (ret && typeof ret.dispose === 'function') dispose = () => ret.dispose();
|
|
1621
|
-
}
|
|
1622
|
-
|
|
1623
|
-
const renderError = (e) => {
|
|
1624
|
-
const pre = document.createElement('pre');
|
|
1625
|
-
pre.style.padding = '12px';
|
|
1626
|
-
pre.style.whiteSpace = 'pre-wrap';
|
|
1627
|
-
pre.textContent = '[sandbox] ' + (e?.stack || e?.message || String(e));
|
|
1628
|
-
container.appendChild(pre);
|
|
1629
|
-
};
|
|
1630
|
-
|
|
1631
|
-
const scheduleReload = (() => {
|
|
1632
|
-
let t = null;
|
|
1633
|
-
return () => {
|
|
1634
|
-
if (t) return;
|
|
1635
|
-
t = setTimeout(() => {
|
|
1636
|
-
t = null;
|
|
1637
|
-
loadAndMount().catch(renderError);
|
|
1638
|
-
}, 80);
|
|
1639
|
-
};
|
|
1640
|
-
})();
|
|
1641
|
-
|
|
1642
|
-
try {
|
|
1643
|
-
const es = new EventSource('/events');
|
|
1644
|
-
es.addEventListener('reload', () => scheduleReload());
|
|
1645
|
-
} catch {
|
|
1646
|
-
// ignore
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
|
-
$('#btnReload').addEventListener('click', () => loadAndMount().catch(renderError));
|
|
1650
|
-
|
|
1651
|
-
loadAndMount().catch(renderError);
|
|
1652
|
-
`;
|
|
1653
|
-
}
|
|
1654
|
-
|
|
1655
|
-
async function loadBackendFactory({ pluginDir, manifest }) {
|
|
1656
|
-
const entryRel = manifest?.backend?.entry ? String(manifest.backend.entry).trim() : '';
|
|
1657
|
-
if (!entryRel) return null;
|
|
1658
|
-
const abs = resolveInsideDir(pluginDir, entryRel);
|
|
1659
|
-
const fileUrl = url.pathToFileURL(abs).toString();
|
|
1660
|
-
const mod = await import(fileUrl + `?t=${Date.now()}`);
|
|
1661
|
-
if (typeof mod?.createUiAppsBackend !== 'function') {
|
|
1662
|
-
throw new Error('backend entry must export createUiAppsBackend(ctx)');
|
|
1663
|
-
}
|
|
1664
|
-
return mod.createUiAppsBackend;
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
export async function startSandboxServer({ pluginDir, port = 4399, appId = '' }) {
|
|
1668
|
-
const { manifest } = loadPluginManifest(pluginDir);
|
|
1669
|
-
const app = pickAppFromManifest(manifest, appId);
|
|
1670
|
-
const effectiveAppId = String(app?.id || '');
|
|
1671
|
-
const entryRel = String(app?.entry?.path || '').trim();
|
|
1672
|
-
if (!entryRel) throw new Error('apps[i].entry.path is required');
|
|
1673
|
-
|
|
1674
|
-
const entryAbs = resolveInsideDir(pluginDir, entryRel);
|
|
1675
|
-
if (!isFile(entryAbs)) throw new Error(`module entry not found: ${entryRel}`);
|
|
1676
|
-
|
|
1760
|
+
|
|
1761
|
+
const events = {
|
|
1762
|
+
subscribe: (filter, fn) => {
|
|
1763
|
+
if (typeof fn !== 'function') throw new Error('listener is required');
|
|
1764
|
+
const sub = { filter: filter && typeof filter === 'object' ? { ...filter } : {}, fn };
|
|
1765
|
+
listeners.add(sub);
|
|
1766
|
+
return () => listeners.delete(sub);
|
|
1767
|
+
},
|
|
1768
|
+
unsubscribe: () => (listeners.clear(), { ok: true }),
|
|
1769
|
+
};
|
|
1770
|
+
|
|
1771
|
+
return {
|
|
1772
|
+
agents: agentsApi,
|
|
1773
|
+
sessions: sessionsApi,
|
|
1774
|
+
messages: messagesApi,
|
|
1775
|
+
send,
|
|
1776
|
+
abort,
|
|
1777
|
+
events,
|
|
1778
|
+
};
|
|
1779
|
+
})(),
|
|
1780
|
+
};
|
|
1781
|
+
|
|
1782
|
+
inspectorEnabled = true;
|
|
1783
|
+
updateInspector();
|
|
1784
|
+
|
|
1785
|
+
let dispose = null;
|
|
1786
|
+
|
|
1787
|
+
async function loadAndMount() {
|
|
1788
|
+
if (typeof dispose === 'function') { try { await dispose(); } catch {} dispose = null; }
|
|
1789
|
+
container.textContent = '';
|
|
1790
|
+
|
|
1791
|
+
const entryUrl = __SANDBOX__.entryUrl;
|
|
1792
|
+
const mod = await import(entryUrl + (entryUrl.includes('?') ? '&' : '?') + 't=' + Date.now());
|
|
1793
|
+
const mount = mod?.mount || mod?.default?.mount || (typeof mod?.default === 'function' ? mod.default : null);
|
|
1794
|
+
if (typeof mount !== 'function') throw new Error('module entry must export mount()');
|
|
1795
|
+
const ret = await mount({ container, host, slots: { header: headerSlot } });
|
|
1796
|
+
if (typeof ret === 'function') dispose = ret;
|
|
1797
|
+
else if (ret && typeof ret.dispose === 'function') dispose = () => ret.dispose();
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
const renderError = (e) => {
|
|
1801
|
+
const pre = document.createElement('pre');
|
|
1802
|
+
pre.style.padding = '12px';
|
|
1803
|
+
pre.style.whiteSpace = 'pre-wrap';
|
|
1804
|
+
pre.textContent = '[sandbox] ' + (e?.stack || e?.message || String(e));
|
|
1805
|
+
container.appendChild(pre);
|
|
1806
|
+
};
|
|
1807
|
+
|
|
1808
|
+
const scheduleReload = (() => {
|
|
1809
|
+
let t = null;
|
|
1810
|
+
return () => {
|
|
1811
|
+
if (t) return;
|
|
1812
|
+
t = setTimeout(() => {
|
|
1813
|
+
t = null;
|
|
1814
|
+
loadAndMount().catch(renderError);
|
|
1815
|
+
}, 80);
|
|
1816
|
+
};
|
|
1817
|
+
})();
|
|
1818
|
+
|
|
1819
|
+
try {
|
|
1820
|
+
const es = new EventSource('/events');
|
|
1821
|
+
es.addEventListener('reload', () => scheduleReload());
|
|
1822
|
+
} catch {
|
|
1823
|
+
// ignore
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
$('#btnReload').addEventListener('click', () => loadAndMount().catch(renderError));
|
|
1827
|
+
|
|
1828
|
+
loadAndMount().catch(renderError);
|
|
1829
|
+
`;
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
async function loadBackendFactory({ pluginDir, manifest }) {
|
|
1833
|
+
const entryRel = manifest?.backend?.entry ? String(manifest.backend.entry).trim() : '';
|
|
1834
|
+
if (!entryRel) return null;
|
|
1835
|
+
const abs = resolveInsideDir(pluginDir, entryRel);
|
|
1836
|
+
const fileUrl = url.pathToFileURL(abs).toString();
|
|
1837
|
+
const mod = await import(fileUrl + `?t=${Date.now()}`);
|
|
1838
|
+
if (typeof mod?.createUiAppsBackend !== 'function') {
|
|
1839
|
+
throw new Error('backend entry must export createUiAppsBackend(ctx)');
|
|
1840
|
+
}
|
|
1841
|
+
return mod.createUiAppsBackend;
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
export async function startSandboxServer({ pluginDir, port = 4399, appId = '' }) {
|
|
1845
|
+
const { manifest } = loadPluginManifest(pluginDir);
|
|
1846
|
+
const app = pickAppFromManifest(manifest, appId);
|
|
1847
|
+
const effectiveAppId = String(app?.id || '');
|
|
1848
|
+
const entryRel = String(app?.entry?.path || '').trim();
|
|
1849
|
+
if (!entryRel) throw new Error('apps[i].entry.path is required');
|
|
1850
|
+
|
|
1851
|
+
const entryAbs = resolveInsideDir(pluginDir, entryRel);
|
|
1852
|
+
if (!isFile(entryAbs)) throw new Error(`module entry not found: ${entryRel}`);
|
|
1853
|
+
|
|
1677
1854
|
const entryUrl = `/plugin/${encodeURIComponent(entryRel).replaceAll('%2F', '/')}`;
|
|
1678
1855
|
|
|
1679
1856
|
let backendInstance = null;
|
|
@@ -1760,15 +1937,12 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
|
|
|
1760
1937
|
if (Object.prototype.hasOwnProperty.call(patch, 'modelId')) {
|
|
1761
1938
|
next.modelId = normalizeText(patch.modelId);
|
|
1762
1939
|
}
|
|
1763
|
-
if (Object.prototype.hasOwnProperty.call(patch, 'workdir')) {
|
|
1764
|
-
next.workdir = normalizeText(patch.workdir);
|
|
1765
|
-
}
|
|
1766
1940
|
sandboxLlmConfig = next;
|
|
1767
1941
|
saveSandboxLlmConfig(sandboxConfigPath, next);
|
|
1768
1942
|
return { ...next };
|
|
1769
1943
|
};
|
|
1770
1944
|
|
|
1771
|
-
const runSandboxChat = async ({ messages, modelId, modelName, systemPrompt, disableTools, signal } = {}) => {
|
|
1945
|
+
const runSandboxChat = async ({ messages, modelId, modelName, systemPrompt, disableTools, signal, callMeta } = {}) => {
|
|
1772
1946
|
const cfg = getSandboxLlmConfig();
|
|
1773
1947
|
const apiKey = normalizeText(cfg.apiKey || process.env.SANDBOX_LLM_API_KEY);
|
|
1774
1948
|
const baseUrl = normalizeText(cfg.baseUrl) || DEFAULT_LLM_BASE_URL;
|
|
@@ -1795,12 +1969,16 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
|
|
|
1795
1969
|
|
|
1796
1970
|
let toolEntries = [];
|
|
1797
1971
|
let toolMap = new Map();
|
|
1972
|
+
let effectiveCallMeta = sandboxCallMeta;
|
|
1798
1973
|
if (!disableTools) {
|
|
1799
1974
|
const runtime = await ensureMcpRuntime();
|
|
1800
1975
|
if (runtime?.toolEntries?.length) {
|
|
1801
1976
|
toolEntries = runtime.toolEntries;
|
|
1802
1977
|
toolMap = runtime.toolMap || new Map();
|
|
1803
1978
|
}
|
|
1979
|
+
if (callMeta && typeof callMeta === 'object') {
|
|
1980
|
+
effectiveCallMeta = mergeCallMeta(sandboxCallMeta, callMeta);
|
|
1981
|
+
}
|
|
1804
1982
|
}
|
|
1805
1983
|
const toolDefs = toolEntries.map((entry) => entry.definition);
|
|
1806
1984
|
|
|
@@ -1843,7 +2021,7 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
|
|
|
1843
2021
|
const toolResult = await toolEntry.client.callTool({
|
|
1844
2022
|
name: toolEntry.toolName,
|
|
1845
2023
|
arguments: args,
|
|
1846
|
-
...(
|
|
2024
|
+
...(effectiveCallMeta ? { _meta: effectiveCallMeta } : {}),
|
|
1847
2025
|
});
|
|
1848
2026
|
resultText = formatMcpToolResult(toolEntry.serverName, toolEntry.toolName, toolResult);
|
|
1849
2027
|
}
|
|
@@ -1864,7 +2042,7 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
|
|
|
1864
2042
|
pluginId: String(manifest?.id || ''),
|
|
1865
2043
|
pluginDir,
|
|
1866
2044
|
stateDir: path.join(sandboxRoot, 'state', 'chatos'),
|
|
1867
|
-
sessionRoot: process.cwd(),
|
|
2045
|
+
sessionRoot: process.cwd(),
|
|
1868
2046
|
projectRoot: process.cwd(),
|
|
1869
2047
|
dataDir: '',
|
|
1870
2048
|
llm: {
|
|
@@ -1872,12 +2050,14 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
|
|
|
1872
2050
|
const input = typeof payload?.input === 'string' ? payload.input : '';
|
|
1873
2051
|
const normalized = String(input || '').trim();
|
|
1874
2052
|
if (!normalized) throw new Error('input is required');
|
|
2053
|
+
const callMeta = payload?.callMeta && typeof payload.callMeta === 'object' ? payload.callMeta : null;
|
|
1875
2054
|
const result = await runSandboxChat({
|
|
1876
2055
|
messages: [{ role: 'user', text: normalized }],
|
|
1877
2056
|
modelId: typeof payload?.modelId === 'string' ? payload.modelId : '',
|
|
1878
2057
|
modelName: typeof payload?.modelName === 'string' ? payload.modelName : '',
|
|
1879
2058
|
systemPrompt: typeof payload?.systemPrompt === 'string' ? payload.systemPrompt : '',
|
|
1880
2059
|
disableTools: payload?.disableTools === true,
|
|
2060
|
+
callMeta,
|
|
1881
2061
|
});
|
|
1882
2062
|
return {
|
|
1883
2063
|
ok: true,
|
|
@@ -1893,7 +2073,6 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
|
|
|
1893
2073
|
ensureDir(ctxBase.dataDir);
|
|
1894
2074
|
sandboxCallMeta = buildSandboxCallMeta({
|
|
1895
2075
|
rawCallMeta: app?.ai?.mcp?.callMeta,
|
|
1896
|
-
rawWorkdir: getSandboxLlmConfig().workdir,
|
|
1897
2076
|
context: {
|
|
1898
2077
|
pluginId: ctxBase.pluginId,
|
|
1899
2078
|
appId: effectiveAppId,
|
|
@@ -1904,30 +2083,30 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
|
|
|
1904
2083
|
projectRoot: ctxBase.projectRoot,
|
|
1905
2084
|
},
|
|
1906
2085
|
});
|
|
1907
|
-
|
|
1908
|
-
const sseClients = new Set();
|
|
1909
|
-
const sseWrite = (res, event, data) => {
|
|
1910
|
-
try {
|
|
1911
|
-
res.write(`event: ${event}\n`);
|
|
1912
|
-
res.write(`data: ${JSON.stringify(data ?? null)}\n\n`);
|
|
1913
|
-
} catch {
|
|
1914
|
-
// ignore
|
|
1915
|
-
}
|
|
1916
|
-
};
|
|
1917
|
-
const sseBroadcast = (event, data) => {
|
|
1918
|
-
for (const res of sseClients) {
|
|
1919
|
-
sseWrite(res, event, data);
|
|
1920
|
-
}
|
|
1921
|
-
};
|
|
1922
|
-
|
|
1923
|
-
let changeSeq = 0;
|
|
1924
|
-
const stopWatch = startRecursiveWatcher(pluginDir, ({ eventType, filePath }) => {
|
|
1925
|
-
const rel = filePath ? path.relative(pluginDir, filePath).replaceAll('\\', '/') : '';
|
|
1926
|
-
const base = rel ? path.basename(rel) : '';
|
|
1927
|
-
if (!rel) return;
|
|
1928
|
-
if (base === '.DS_Store') return;
|
|
1929
|
-
if (base.endsWith('.map')) return;
|
|
1930
|
-
|
|
2086
|
+
|
|
2087
|
+
const sseClients = new Set();
|
|
2088
|
+
const sseWrite = (res, event, data) => {
|
|
2089
|
+
try {
|
|
2090
|
+
res.write(`event: ${event}\n`);
|
|
2091
|
+
res.write(`data: ${JSON.stringify(data ?? null)}\n\n`);
|
|
2092
|
+
} catch {
|
|
2093
|
+
// ignore
|
|
2094
|
+
}
|
|
2095
|
+
};
|
|
2096
|
+
const sseBroadcast = (event, data) => {
|
|
2097
|
+
for (const res of sseClients) {
|
|
2098
|
+
sseWrite(res, event, data);
|
|
2099
|
+
}
|
|
2100
|
+
};
|
|
2101
|
+
|
|
2102
|
+
let changeSeq = 0;
|
|
2103
|
+
const stopWatch = startRecursiveWatcher(pluginDir, ({ eventType, filePath }) => {
|
|
2104
|
+
const rel = filePath ? path.relative(pluginDir, filePath).replaceAll('\\', '/') : '';
|
|
2105
|
+
const base = rel ? path.basename(rel) : '';
|
|
2106
|
+
if (!rel) return;
|
|
2107
|
+
if (base === '.DS_Store') return;
|
|
2108
|
+
if (base.endsWith('.map')) return;
|
|
2109
|
+
|
|
1931
2110
|
changeSeq += 1;
|
|
1932
2111
|
if (rel.startsWith('backend/')) {
|
|
1933
2112
|
backendInstance = null;
|
|
@@ -1936,71 +2115,69 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
|
|
|
1936
2115
|
resetMcpRuntime().catch(() => {});
|
|
1937
2116
|
sseBroadcast('reload', { seq: changeSeq, eventType: eventType || '', path: rel });
|
|
1938
2117
|
});
|
|
1939
|
-
|
|
1940
|
-
const server = http.createServer(async (req, res) => {
|
|
1941
|
-
try {
|
|
1942
|
-
const parsed = url.parse(req.url || '/', true);
|
|
1943
|
-
const pathname = parsed.pathname || '/';
|
|
1944
|
-
|
|
1945
|
-
if (req.method === 'GET' && pathname === '/') {
|
|
1946
|
-
return sendText(res, 200, htmlPage(), 'text/html; charset=utf-8');
|
|
1947
|
-
}
|
|
1948
|
-
|
|
1949
|
-
if (req.method === 'GET' && pathname === '/events') {
|
|
1950
|
-
res.writeHead(200, {
|
|
1951
|
-
'content-type': 'text/event-stream; charset=utf-8',
|
|
1952
|
-
'cache-control': 'no-store',
|
|
1953
|
-
connection: 'keep-alive',
|
|
1954
|
-
});
|
|
1955
|
-
res.write(': connected\n\n');
|
|
1956
|
-
sseClients.add(res);
|
|
1957
|
-
const ping = setInterval(() => {
|
|
1958
|
-
try {
|
|
1959
|
-
res.write(': ping\n\n');
|
|
1960
|
-
} catch {
|
|
1961
|
-
// ignore
|
|
1962
|
-
}
|
|
1963
|
-
}, 15000);
|
|
1964
|
-
req.on('close', () => {
|
|
1965
|
-
try {
|
|
1966
|
-
clearInterval(ping);
|
|
1967
|
-
} catch {
|
|
1968
|
-
// ignore
|
|
1969
|
-
}
|
|
1970
|
-
sseClients.delete(res);
|
|
1971
|
-
});
|
|
1972
|
-
return;
|
|
1973
|
-
}
|
|
1974
|
-
|
|
2118
|
+
|
|
2119
|
+
const server = http.createServer(async (req, res) => {
|
|
2120
|
+
try {
|
|
2121
|
+
const parsed = url.parse(req.url || '/', true);
|
|
2122
|
+
const pathname = parsed.pathname || '/';
|
|
2123
|
+
|
|
2124
|
+
if (req.method === 'GET' && pathname === '/') {
|
|
2125
|
+
return sendText(res, 200, htmlPage(), 'text/html; charset=utf-8');
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
if (req.method === 'GET' && pathname === '/events') {
|
|
2129
|
+
res.writeHead(200, {
|
|
2130
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
2131
|
+
'cache-control': 'no-store',
|
|
2132
|
+
connection: 'keep-alive',
|
|
2133
|
+
});
|
|
2134
|
+
res.write(': connected\n\n');
|
|
2135
|
+
sseClients.add(res);
|
|
2136
|
+
const ping = setInterval(() => {
|
|
2137
|
+
try {
|
|
2138
|
+
res.write(': ping\n\n');
|
|
2139
|
+
} catch {
|
|
2140
|
+
// ignore
|
|
2141
|
+
}
|
|
2142
|
+
}, 15000);
|
|
2143
|
+
req.on('close', () => {
|
|
2144
|
+
try {
|
|
2145
|
+
clearInterval(ping);
|
|
2146
|
+
} catch {
|
|
2147
|
+
// ignore
|
|
2148
|
+
}
|
|
2149
|
+
sseClients.delete(res);
|
|
2150
|
+
});
|
|
2151
|
+
return;
|
|
2152
|
+
}
|
|
2153
|
+
|
|
1975
2154
|
if (req.method === 'GET' && pathname === '/sandbox.mjs') {
|
|
1976
2155
|
const tokenNames = loadTokenNames();
|
|
1977
|
-
const
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
pluginDir: ctxBase.pluginDir,
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
projectRoot: ctxBase.projectRoot,
|
|
1985
|
-
workdir: sandboxCallMeta?.workdir || ctxBase.dataDir || '',
|
|
2156
|
+
const sandboxPaths = {
|
|
2157
|
+
workdir: sandboxCallMeta?.workdir || '',
|
|
2158
|
+
dataDir: ctxBase.dataDir || '',
|
|
2159
|
+
pluginDir: ctxBase.pluginDir || '',
|
|
2160
|
+
stateDir: ctxBase.stateDir || '',
|
|
2161
|
+
sessionRoot: ctxBase.sessionRoot || '',
|
|
2162
|
+
projectRoot: ctxBase.projectRoot || '',
|
|
1986
2163
|
};
|
|
1987
2164
|
const js = sandboxClientJs()
|
|
1988
|
-
.replaceAll('__SANDBOX__.context', JSON.stringify(sandboxContext))
|
|
1989
2165
|
.replaceAll('__SANDBOX__.pluginId', JSON.stringify(ctxBase.pluginId))
|
|
1990
2166
|
.replaceAll('__SANDBOX__.appId', JSON.stringify(effectiveAppId))
|
|
1991
2167
|
.replaceAll('__SANDBOX__.entryUrl', JSON.stringify(entryUrl))
|
|
1992
2168
|
.replaceAll('__SANDBOX__.registryApp', JSON.stringify({ plugin: { id: ctxBase.pluginId }, id: effectiveAppId, entry: { type: 'module', url: entryUrl } }))
|
|
1993
|
-
.replaceAll('__SANDBOX__.tokenNames', JSON.stringify(tokenNames))
|
|
2169
|
+
.replaceAll('__SANDBOX__.tokenNames', JSON.stringify(tokenNames))
|
|
2170
|
+
.replaceAll('__SANDBOX__.paths', JSON.stringify(sandboxPaths));
|
|
1994
2171
|
return sendText(res, 200, js, 'text/javascript; charset=utf-8');
|
|
1995
2172
|
}
|
|
1996
|
-
|
|
1997
|
-
if (req.method === 'GET' && pathname.startsWith('/plugin/')) {
|
|
1998
|
-
const rel = decodeURIComponent(pathname.slice('/plugin/'.length));
|
|
1999
|
-
const abs = resolveInsideDir(pluginDir, rel);
|
|
2000
|
-
if (!serveStaticFile(res, abs)) return sendText(res, 404, 'Not found');
|
|
2001
|
-
return;
|
|
2002
|
-
}
|
|
2003
|
-
|
|
2173
|
+
|
|
2174
|
+
if (req.method === 'GET' && pathname.startsWith('/plugin/')) {
|
|
2175
|
+
const rel = decodeURIComponent(pathname.slice('/plugin/'.length));
|
|
2176
|
+
const abs = resolveInsideDir(pluginDir, rel);
|
|
2177
|
+
if (!serveStaticFile(res, abs)) return sendText(res, 404, 'Not found');
|
|
2178
|
+
return;
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2004
2181
|
if (req.method === 'GET' && pathname === '/api/manifest') {
|
|
2005
2182
|
return sendJson(res, 200, { ok: true, manifest });
|
|
2006
2183
|
}
|
|
@@ -2013,7 +2190,6 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
|
|
|
2013
2190
|
config: {
|
|
2014
2191
|
baseUrl: cfg.baseUrl || '',
|
|
2015
2192
|
modelId: cfg.modelId || '',
|
|
2016
|
-
workdir: cfg.workdir || '',
|
|
2017
2193
|
hasApiKey: Boolean(cfg.apiKey),
|
|
2018
2194
|
},
|
|
2019
2195
|
});
|
|
@@ -2026,14 +2202,12 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
|
|
|
2026
2202
|
...(Object.prototype.hasOwnProperty.call(patch || {}, 'apiKey') ? { apiKey: patch.apiKey } : {}),
|
|
2027
2203
|
...(Object.prototype.hasOwnProperty.call(patch || {}, 'baseUrl') ? { baseUrl: patch.baseUrl } : {}),
|
|
2028
2204
|
...(Object.prototype.hasOwnProperty.call(patch || {}, 'modelId') ? { modelId: patch.modelId } : {}),
|
|
2029
|
-
...(Object.prototype.hasOwnProperty.call(patch || {}, 'workdir') ? { workdir: patch.workdir } : {}),
|
|
2030
2205
|
});
|
|
2031
2206
|
return sendJson(res, 200, {
|
|
2032
2207
|
ok: true,
|
|
2033
2208
|
config: {
|
|
2034
2209
|
baseUrl: next.baseUrl || '',
|
|
2035
2210
|
modelId: next.modelId || '',
|
|
2036
|
-
workdir: next.workdir || '',
|
|
2037
2211
|
hasApiKey: Boolean(next.apiKey),
|
|
2038
2212
|
},
|
|
2039
2213
|
});
|
|
@@ -2049,12 +2223,14 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
|
|
|
2049
2223
|
try {
|
|
2050
2224
|
const payload = await readJsonBody(req);
|
|
2051
2225
|
const messages = Array.isArray(payload?.messages) ? payload.messages : [];
|
|
2226
|
+
const callMeta = payload?.callMeta && typeof payload.callMeta === 'object' ? payload.callMeta : null;
|
|
2052
2227
|
const result = await runSandboxChat({
|
|
2053
2228
|
messages,
|
|
2054
2229
|
modelId: typeof payload?.modelId === 'string' ? payload.modelId : '',
|
|
2055
2230
|
modelName: typeof payload?.modelName === 'string' ? payload.modelName : '',
|
|
2056
2231
|
systemPrompt: typeof payload?.systemPrompt === 'string' ? payload.systemPrompt : '',
|
|
2057
2232
|
disableTools: payload?.disableTools === true,
|
|
2233
|
+
callMeta,
|
|
2058
2234
|
});
|
|
2059
2235
|
return sendJson(res, 200, {
|
|
2060
2236
|
ok: true,
|
|
@@ -2072,55 +2248,55 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
|
|
|
2072
2248
|
let body = '';
|
|
2073
2249
|
req.on('data', (chunk) => {
|
|
2074
2250
|
body += chunk;
|
|
2075
|
-
});
|
|
2076
|
-
req.on('end', async () => {
|
|
2077
|
-
try {
|
|
2078
|
-
const payload = body ? JSON.parse(body) : {};
|
|
2079
|
-
const method = typeof payload?.method === 'string' ? payload.method.trim() : '';
|
|
2080
|
-
if (!method) return sendJson(res, 400, { ok: false, message: 'method is required' });
|
|
2081
|
-
const params = payload?.params;
|
|
2082
|
-
|
|
2083
|
-
if (!backendFactory) backendFactory = await loadBackendFactory({ pluginDir, manifest });
|
|
2084
|
-
if (!backendFactory) return sendJson(res, 200, { ok: false, message: 'backend not configured in plugin.json' });
|
|
2085
|
-
|
|
2086
|
-
if (!backendInstance || typeof backendInstance !== 'object' || !backendInstance.methods) {
|
|
2087
|
-
backendInstance = await backendFactory({ ...ctxBase });
|
|
2088
|
-
}
|
|
2089
|
-
const fn = backendInstance?.methods?.[method];
|
|
2090
|
-
if (typeof fn !== 'function') return sendJson(res, 404, { ok: false, message: `method not found: ${method}` });
|
|
2091
|
-
const result = await fn(params, { ...ctxBase });
|
|
2092
|
-
return sendJson(res, 200, { ok: true, result });
|
|
2093
|
-
} catch (e) {
|
|
2094
|
-
return sendJson(res, 200, { ok: false, message: e?.message || String(e) });
|
|
2095
|
-
}
|
|
2096
|
-
});
|
|
2097
|
-
return;
|
|
2098
|
-
}
|
|
2099
|
-
|
|
2100
|
-
sendText(res, 404, 'Not found');
|
|
2101
|
-
} catch (e) {
|
|
2102
|
-
sendJson(res, 500, { ok: false, message: e?.message || String(e) });
|
|
2103
|
-
}
|
|
2104
|
-
});
|
|
2251
|
+
});
|
|
2252
|
+
req.on('end', async () => {
|
|
2253
|
+
try {
|
|
2254
|
+
const payload = body ? JSON.parse(body) : {};
|
|
2255
|
+
const method = typeof payload?.method === 'string' ? payload.method.trim() : '';
|
|
2256
|
+
if (!method) return sendJson(res, 400, { ok: false, message: 'method is required' });
|
|
2257
|
+
const params = payload?.params;
|
|
2258
|
+
|
|
2259
|
+
if (!backendFactory) backendFactory = await loadBackendFactory({ pluginDir, manifest });
|
|
2260
|
+
if (!backendFactory) return sendJson(res, 200, { ok: false, message: 'backend not configured in plugin.json' });
|
|
2261
|
+
|
|
2262
|
+
if (!backendInstance || typeof backendInstance !== 'object' || !backendInstance.methods) {
|
|
2263
|
+
backendInstance = await backendFactory({ ...ctxBase });
|
|
2264
|
+
}
|
|
2265
|
+
const fn = backendInstance?.methods?.[method];
|
|
2266
|
+
if (typeof fn !== 'function') return sendJson(res, 404, { ok: false, message: `method not found: ${method}` });
|
|
2267
|
+
const result = await fn(params, { ...ctxBase });
|
|
2268
|
+
return sendJson(res, 200, { ok: true, result });
|
|
2269
|
+
} catch (e) {
|
|
2270
|
+
return sendJson(res, 200, { ok: false, message: e?.message || String(e) });
|
|
2271
|
+
}
|
|
2272
|
+
});
|
|
2273
|
+
return;
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
sendText(res, 404, 'Not found');
|
|
2277
|
+
} catch (e) {
|
|
2278
|
+
sendJson(res, 500, { ok: false, message: e?.message || String(e) });
|
|
2279
|
+
}
|
|
2280
|
+
});
|
|
2105
2281
|
server.once('close', () => {
|
|
2106
2282
|
stopWatch();
|
|
2107
2283
|
resetMcpRuntime().catch(() => {});
|
|
2108
2284
|
});
|
|
2109
|
-
|
|
2110
|
-
await new Promise((resolve, reject) => {
|
|
2111
|
-
server.once('error', reject);
|
|
2112
|
-
server.listen(port, '127.0.0.1', () => {
|
|
2113
|
-
server.off('error', reject);
|
|
2114
|
-
resolve();
|
|
2115
|
-
});
|
|
2116
|
-
});
|
|
2117
|
-
|
|
2118
|
-
// eslint-disable-next-line no-console
|
|
2119
|
-
console.log(`Sandbox running:
|
|
2120
|
-
http://localhost:${port}/
|
|
2121
|
-
pluginDir:
|
|
2122
|
-
${pluginDir}
|
|
2123
|
-
app:
|
|
2124
|
-
${ctxBase.pluginId}:${effectiveAppId}
|
|
2125
|
-
`);
|
|
2126
|
-
}
|
|
2285
|
+
|
|
2286
|
+
await new Promise((resolve, reject) => {
|
|
2287
|
+
server.once('error', reject);
|
|
2288
|
+
server.listen(port, '127.0.0.1', () => {
|
|
2289
|
+
server.off('error', reject);
|
|
2290
|
+
resolve();
|
|
2291
|
+
});
|
|
2292
|
+
});
|
|
2293
|
+
|
|
2294
|
+
// eslint-disable-next-line no-console
|
|
2295
|
+
console.log(`Sandbox running:
|
|
2296
|
+
http://localhost:${port}/
|
|
2297
|
+
pluginDir:
|
|
2298
|
+
${pluginDir}
|
|
2299
|
+
app:
|
|
2300
|
+
${ctxBase.pluginId}:${effectiveAppId}
|
|
2301
|
+
`);
|
|
2302
|
+
}
|