@leeoohoo/ui-apps-devkit 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -60
- package/bin/chatos-uiapp.js +4 -4
- package/package.json +26 -20
- package/src/cli.js +53 -53
- package/src/commands/dev.js +14 -14
- package/src/commands/init.js +131 -129
- package/src/commands/install.js +47 -46
- package/src/commands/pack.js +72 -72
- package/src/commands/validate.js +138 -80
- package/src/lib/args.js +49 -49
- package/src/lib/config.js +29 -29
- package/src/lib/fs.js +78 -78
- package/src/lib/path-boundary.js +16 -16
- package/src/lib/plugin.js +45 -45
- package/src/lib/state-constants.js +2 -0
- package/src/lib/template.js +172 -168
- package/src/sandbox/server.js +1957 -692
- package/templates/basic/README.md +78 -54
- package/templates/basic/chatos.config.json +5 -5
- package/templates/basic/docs/CHATOS_UI_APPS_AI_CONTRIBUTIONS.md +214 -181
- package/templates/basic/docs/CHATOS_UI_APPS_BACKEND_PROTOCOL.md +75 -74
- package/templates/basic/docs/CHATOS_UI_APPS_HOST_API.md +136 -123
- package/templates/basic/docs/CHATOS_UI_APPS_OVERVIEW.md +112 -107
- package/templates/basic/docs/CHATOS_UI_APPS_PLUGIN_MANIFEST.md +242 -227
- package/templates/basic/docs/CHATOS_UI_APPS_STYLE_GUIDE.md +95 -0
- package/templates/basic/docs/CHATOS_UI_APPS_TROUBLESHOOTING.md +45 -0
- package/templates/basic/docs/CHATOS_UI_PROMPTS_PROTOCOL.md +392 -392
- package/templates/basic/plugin/apps/app/compact.mjs +41 -0
- package/templates/basic/plugin/apps/app/index.mjs +287 -263
- package/templates/basic/plugin/apps/app/mcp-prompt.en.md +7 -7
- package/templates/basic/plugin/apps/app/mcp-prompt.zh.md +7 -7
- package/templates/basic/plugin/apps/app/mcp-server.mjs +15 -15
- package/templates/basic/plugin/backend/index.mjs +37 -37
- package/templates/basic/template.json +7 -7
- package/templates/notepad/README.md +55 -24
- package/templates/notepad/chatos.config.json +4 -4
- package/templates/notepad/docs/CHATOS_UI_APPS_AI_CONTRIBUTIONS.md +214 -181
- package/templates/notepad/docs/CHATOS_UI_APPS_BACKEND_PROTOCOL.md +75 -74
- package/templates/notepad/docs/CHATOS_UI_APPS_HOST_API.md +136 -123
- package/templates/notepad/docs/CHATOS_UI_APPS_OVERVIEW.md +112 -107
- package/templates/notepad/docs/CHATOS_UI_APPS_PLUGIN_MANIFEST.md +242 -227
- package/templates/notepad/docs/CHATOS_UI_APPS_STYLE_GUIDE.md +95 -0
- package/templates/notepad/docs/CHATOS_UI_APPS_TROUBLESHOOTING.md +45 -0
- package/templates/notepad/docs/CHATOS_UI_PROMPTS_PROTOCOL.md +392 -392
- package/templates/notepad/plugin/apps/app/api.mjs +30 -30
- package/templates/notepad/plugin/apps/app/compact.mjs +41 -0
- package/templates/notepad/plugin/apps/app/dom.mjs +14 -14
- package/templates/notepad/plugin/apps/app/ds-tree.mjs +35 -35
- package/templates/notepad/plugin/apps/app/index.mjs +1056 -1056
- package/templates/notepad/plugin/apps/app/layers.mjs +338 -338
- package/templates/notepad/plugin/apps/app/markdown.mjs +120 -120
- package/templates/notepad/plugin/apps/app/mcp-prompt.en.md +22 -22
- package/templates/notepad/plugin/apps/app/mcp-prompt.zh.md +22 -22
- package/templates/notepad/plugin/apps/app/mcp-server.mjs +206 -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 +59 -41
- package/templates/notepad/plugin/shared/notepad-store.mjs +765 -765
- package/templates/notepad/template.json +8 -8
package/src/sandbox/server.js
CHANGED
|
@@ -1,188 +1,783 @@
|
|
|
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';
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
7
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
8
|
+
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
9
|
+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
10
|
+
import { WebSocketClientTransport } from '@modelcontextprotocol/sdk/client/websocket.js';
|
|
11
|
+
|
|
12
|
+
import { copyDir, ensureDir, isDirectory, isFile } from '../lib/fs.js';
|
|
7
13
|
import { loadPluginManifest, pickAppFromManifest } from '../lib/plugin.js';
|
|
8
14
|
import { resolveInsideDir } from '../lib/path-boundary.js';
|
|
15
|
+
import { COMPAT_STATE_ROOT_DIRNAME, STATE_ROOT_DIRNAME } from '../lib/state-constants.js';
|
|
16
|
+
|
|
17
|
+
const __filename = url.fileURLToPath(import.meta.url);
|
|
18
|
+
const __dirname = path.dirname(__filename);
|
|
19
|
+
|
|
20
|
+
const TOKEN_REGEX = /--ds-[a-z0-9-]+/gi;
|
|
21
|
+
const SANDBOX_STATE_DIRNAME = STATE_ROOT_DIRNAME;
|
|
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
|
+
|
|
28
|
+
function loadTokenNames() {
|
|
29
|
+
for (const candidate of GLOBAL_STYLES_CANDIDATES) {
|
|
30
|
+
try {
|
|
31
|
+
if (!isFile(candidate)) continue;
|
|
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
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
9
42
|
|
|
10
|
-
function
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
43
|
+
function resolveSandboxRoots() {
|
|
44
|
+
const cwd = process.cwd();
|
|
45
|
+
const primary = path.join(cwd, SANDBOX_STATE_DIRNAME);
|
|
46
|
+
const legacy = path.join(cwd, SANDBOX_COMPAT_DIRNAME);
|
|
47
|
+
if (!isDirectory(primary) && isDirectory(legacy)) {
|
|
48
|
+
try {
|
|
49
|
+
copyDir(legacy, primary);
|
|
50
|
+
} catch {
|
|
51
|
+
// ignore compat copy errors
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { primary, legacy };
|
|
17
55
|
}
|
|
18
56
|
|
|
19
|
-
function
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
57
|
+
function resolveSandboxConfigPath({ primaryRoot, legacyRoot }) {
|
|
58
|
+
const primaryPath = path.join(primaryRoot, 'sandbox', 'llm-config.json');
|
|
59
|
+
if (isFile(primaryPath)) return primaryPath;
|
|
60
|
+
const legacyPath = path.join(legacyRoot, 'sandbox', 'llm-config.json');
|
|
61
|
+
if (isFile(legacyPath)) return legacyPath;
|
|
62
|
+
return primaryPath;
|
|
25
63
|
}
|
|
26
64
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (ext === '.mjs' || ext === '.js') return 'text/javascript; charset=utf-8';
|
|
32
|
-
if (ext === '.json') return 'application/json; charset=utf-8';
|
|
33
|
-
if (ext === '.md') return 'text/markdown; charset=utf-8';
|
|
34
|
-
if (ext === '.svg') return 'image/svg+xml';
|
|
35
|
-
if (ext === '.png') return 'image/png';
|
|
36
|
-
return 'application/octet-stream';
|
|
65
|
+
const DEFAULT_LLM_BASE_URL = 'https://api.openai.com/v1';
|
|
66
|
+
|
|
67
|
+
function normalizeText(value) {
|
|
68
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
37
69
|
}
|
|
38
70
|
|
|
39
|
-
function
|
|
40
|
-
|
|
41
|
-
const ct = guessContentType(filePath);
|
|
42
|
-
const buf = fs.readFileSync(filePath);
|
|
43
|
-
res.writeHead(200, { 'content-type': ct, 'cache-control': 'no-store' });
|
|
44
|
-
res.end(buf);
|
|
45
|
-
return true;
|
|
71
|
+
function isPlainObject(value) {
|
|
72
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
46
73
|
}
|
|
47
74
|
|
|
48
|
-
function
|
|
49
|
-
|
|
50
|
-
|
|
75
|
+
function cloneValue(value) {
|
|
76
|
+
if (Array.isArray(value)) {
|
|
77
|
+
return value.map((entry) => cloneValue(entry));
|
|
78
|
+
}
|
|
79
|
+
if (isPlainObject(value)) {
|
|
80
|
+
const out = {};
|
|
81
|
+
Object.entries(value).forEach(([key, entry]) => {
|
|
82
|
+
out[key] = cloneValue(entry);
|
|
83
|
+
});
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
51
88
|
|
|
52
|
-
|
|
89
|
+
function mergeCallMeta(base, override) {
|
|
90
|
+
if (!base && !override) return null;
|
|
91
|
+
if (!base) return cloneValue(override);
|
|
92
|
+
if (!override) return cloneValue(base);
|
|
93
|
+
if (!isPlainObject(base) || !isPlainObject(override)) {
|
|
94
|
+
return cloneValue(override);
|
|
95
|
+
}
|
|
96
|
+
const merged = cloneValue(base);
|
|
97
|
+
Object.entries(override).forEach(([key, value]) => {
|
|
98
|
+
if (isPlainObject(value) && isPlainObject(merged[key])) {
|
|
99
|
+
merged[key] = mergeCallMeta(merged[key], value);
|
|
100
|
+
} else {
|
|
101
|
+
merged[key] = cloneValue(value);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
return merged;
|
|
105
|
+
}
|
|
53
106
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
107
|
+
function expandCallMetaValue(value, vars) {
|
|
108
|
+
if (typeof value === 'string') {
|
|
109
|
+
let text = value;
|
|
110
|
+
Object.entries(vars).forEach(([key, replacement]) => {
|
|
111
|
+
const token = `$${key}`;
|
|
112
|
+
text = text.split(token).join(String(replacement || ''));
|
|
113
|
+
});
|
|
114
|
+
return text;
|
|
115
|
+
}
|
|
116
|
+
if (Array.isArray(value)) {
|
|
117
|
+
return value.map((entry) => expandCallMetaValue(entry, vars));
|
|
118
|
+
}
|
|
119
|
+
if (isPlainObject(value)) {
|
|
120
|
+
const out = {};
|
|
121
|
+
Object.entries(value).forEach(([key, entry]) => {
|
|
122
|
+
out[key] = expandCallMetaValue(entry, vars);
|
|
123
|
+
});
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
return value;
|
|
127
|
+
}
|
|
62
128
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
watchers.set(abs, w);
|
|
80
|
-
} catch {
|
|
81
|
-
// ignore
|
|
129
|
+
function buildSandboxCallMeta({ rawCallMeta, rawWorkdir, context } = {}) {
|
|
130
|
+
const ctx = context && typeof context === 'object' ? context : null;
|
|
131
|
+
const defaults = ctx
|
|
132
|
+
? {
|
|
133
|
+
chatos: {
|
|
134
|
+
uiApp: {
|
|
135
|
+
...(ctx.pluginId ? { pluginId: ctx.pluginId } : null),
|
|
136
|
+
...(ctx.appId ? { appId: ctx.appId } : null),
|
|
137
|
+
...(ctx.pluginDir ? { pluginDir: ctx.pluginDir } : null),
|
|
138
|
+
...(ctx.dataDir ? { dataDir: ctx.dataDir } : null),
|
|
139
|
+
...(ctx.stateDir ? { stateDir: ctx.stateDir } : null),
|
|
140
|
+
...(ctx.sessionRoot ? { sessionRoot: ctx.sessionRoot } : null),
|
|
141
|
+
...(ctx.projectRoot ? { projectRoot: ctx.projectRoot } : null),
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
workdir: ctx.dataDir || ctx.pluginDir || ctx.projectRoot || ctx.sessionRoot || '',
|
|
82
145
|
}
|
|
146
|
+
: null;
|
|
147
|
+
const raw = rawCallMeta && typeof rawCallMeta === 'object' ? rawCallMeta : null;
|
|
148
|
+
if (!defaults && !raw) return null;
|
|
149
|
+
const vars = ctx
|
|
150
|
+
? {
|
|
151
|
+
pluginId: ctx.pluginId || '',
|
|
152
|
+
appId: ctx.appId || '',
|
|
153
|
+
pluginDir: ctx.pluginDir || '',
|
|
154
|
+
dataDir: ctx.dataDir || '',
|
|
155
|
+
stateDir: ctx.stateDir || '',
|
|
156
|
+
sessionRoot: ctx.sessionRoot || '',
|
|
157
|
+
projectRoot: ctx.projectRoot || '',
|
|
158
|
+
}
|
|
159
|
+
: {};
|
|
160
|
+
const expanded = raw ? expandCallMetaValue(raw, vars) : null;
|
|
161
|
+
let merged = mergeCallMeta(defaults, expanded);
|
|
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 });
|
|
83
168
|
}
|
|
169
|
+
}
|
|
170
|
+
return merged;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function loadSandboxLlmConfig(filePath) {
|
|
174
|
+
if (!filePath) return { apiKey: '', baseUrl: '', modelId: '', workdir: '' };
|
|
175
|
+
try {
|
|
176
|
+
if (!fs.existsSync(filePath)) return { apiKey: '', baseUrl: '', modelId: '', workdir: '' };
|
|
177
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
178
|
+
const parsed = raw ? JSON.parse(raw) : {};
|
|
179
|
+
return {
|
|
180
|
+
apiKey: normalizeText(parsed?.apiKey),
|
|
181
|
+
baseUrl: normalizeText(parsed?.baseUrl),
|
|
182
|
+
modelId: normalizeText(parsed?.modelId),
|
|
183
|
+
workdir: normalizeText(parsed?.workdir),
|
|
184
|
+
};
|
|
185
|
+
} catch {
|
|
186
|
+
return { apiKey: '', baseUrl: '', modelId: '', workdir: '' };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function saveSandboxLlmConfig(filePath, config) {
|
|
191
|
+
if (!filePath) return;
|
|
192
|
+
try {
|
|
193
|
+
ensureDir(path.dirname(filePath));
|
|
194
|
+
fs.writeFileSync(filePath, JSON.stringify(config || {}, null, 2), 'utf8');
|
|
195
|
+
} catch {
|
|
196
|
+
// ignore
|
|
197
|
+
}
|
|
198
|
+
}
|
|
84
199
|
|
|
85
|
-
|
|
200
|
+
function resolveChatCompletionsUrl(baseUrl) {
|
|
201
|
+
const raw = normalizeText(baseUrl);
|
|
202
|
+
if (!raw) return `${DEFAULT_LLM_BASE_URL}/chat/completions`;
|
|
203
|
+
const normalized = raw.replace(/\/+$/g, '');
|
|
204
|
+
if (normalized.endsWith('/chat/completions')) return normalized;
|
|
205
|
+
if (normalized.includes('/v1')) return `${normalized}/chat/completions`;
|
|
206
|
+
return `${normalized}/v1/chat/completions`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function normalizeMcpName(value) {
|
|
210
|
+
return String(value || '')
|
|
211
|
+
.trim()
|
|
212
|
+
.toLowerCase()
|
|
213
|
+
.replace(/[^a-z0-9_-]+/g, '_')
|
|
214
|
+
.replace(/^_+|_+$/g, '');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function buildMcpToolIdentifier(serverName, toolName) {
|
|
218
|
+
const server = normalizeMcpName(serverName) || 'mcp_server';
|
|
219
|
+
const tool = normalizeMcpName(toolName) || 'tool';
|
|
220
|
+
return `mcp_${server}_${tool}`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function buildMcpToolDescription(serverName, tool) {
|
|
224
|
+
const parts = [];
|
|
225
|
+
if (serverName) parts.push(`[${serverName}]`);
|
|
226
|
+
if (tool?.annotations?.title) parts.push(tool.annotations.title);
|
|
227
|
+
else if (tool?.description) parts.push(tool.description);
|
|
228
|
+
else parts.push('MCP tool');
|
|
229
|
+
return parts.join(' ');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function extractContentText(blocks) {
|
|
233
|
+
if (!Array.isArray(blocks) || blocks.length === 0) return '';
|
|
234
|
+
const lines = [];
|
|
235
|
+
blocks.forEach((block) => {
|
|
236
|
+
if (!block || typeof block !== 'object') return;
|
|
237
|
+
switch (block.type) {
|
|
238
|
+
case 'text':
|
|
239
|
+
if (block.text) lines.push(block.text);
|
|
240
|
+
break;
|
|
241
|
+
case 'resource_link':
|
|
242
|
+
lines.push(`resource: ${block.uri || block.resourceId || '(unknown)'}`);
|
|
243
|
+
break;
|
|
244
|
+
case 'image':
|
|
245
|
+
lines.push(`image (${block.mimeType || 'image'}, ${approxSize(block.data)})`);
|
|
246
|
+
break;
|
|
247
|
+
case 'audio':
|
|
248
|
+
lines.push(`audio (${block.mimeType || 'audio'}, ${approxSize(block.data)})`);
|
|
249
|
+
break;
|
|
250
|
+
case 'resource':
|
|
251
|
+
lines.push('resource payload returned (use /mcp to inspect).');
|
|
252
|
+
break;
|
|
253
|
+
default:
|
|
254
|
+
lines.push(`[${block.type}]`);
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
return lines.join('\n');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function approxSize(base64Text) {
|
|
262
|
+
if (!base64Text) return 'unknown size';
|
|
263
|
+
const bytes = Math.round((base64Text.length * 3) / 4);
|
|
264
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
265
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
266
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function formatMcpToolResult(serverName, toolName, result) {
|
|
270
|
+
const header = `[${serverName}/${toolName}]`;
|
|
271
|
+
if (!result) return `${header} tool returned no result.`;
|
|
272
|
+
if (result.isError) {
|
|
273
|
+
const errorText = extractContentText(result.content) || 'MCP tool failed.';
|
|
274
|
+
return `${header} ❌ ${errorText}`;
|
|
275
|
+
}
|
|
276
|
+
const segments = [];
|
|
277
|
+
const textBlock = extractContentText(result.content);
|
|
278
|
+
if (textBlock) segments.push(textBlock);
|
|
279
|
+
if (result.structuredContent && Object.keys(result.structuredContent).length > 0) {
|
|
280
|
+
segments.push(JSON.stringify(result.structuredContent, null, 2));
|
|
281
|
+
}
|
|
282
|
+
if (segments.length === 0) segments.push('Tool completed with no text output.');
|
|
283
|
+
return `${header}\n${segments.join('\n\n')}`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function listAllMcpTools(client) {
|
|
287
|
+
const collected = [];
|
|
288
|
+
let cursor = null;
|
|
289
|
+
do {
|
|
290
|
+
// eslint-disable-next-line no-await-in-loop
|
|
291
|
+
const result = await client.listTools(cursor ? { cursor } : undefined);
|
|
292
|
+
if (Array.isArray(result?.tools)) {
|
|
293
|
+
collected.push(...result.tools);
|
|
294
|
+
}
|
|
295
|
+
cursor = result?.nextCursor || null;
|
|
296
|
+
} while (cursor);
|
|
297
|
+
if (typeof client.cacheToolMetadata === 'function') {
|
|
298
|
+
client.cacheToolMetadata(collected);
|
|
299
|
+
}
|
|
300
|
+
return collected;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function connectMcpServer(entry) {
|
|
304
|
+
if (!entry || typeof entry !== 'object') return null;
|
|
305
|
+
const serverName = normalizeText(entry.name) || 'mcp_server';
|
|
306
|
+
const env = { ...process.env };
|
|
307
|
+
if (!env.MODEL_CLI_SESSION_ROOT) env.MODEL_CLI_SESSION_ROOT = process.cwd();
|
|
308
|
+
if (!env.MODEL_CLI_WORKSPACE_ROOT) env.MODEL_CLI_WORKSPACE_ROOT = process.cwd();
|
|
309
|
+
|
|
310
|
+
if (entry.command) {
|
|
311
|
+
const client = new Client({ name: 'sandbox', version: '0.1.0' });
|
|
312
|
+
const transport = new StdioClientTransport({
|
|
313
|
+
command: entry.command,
|
|
314
|
+
args: Array.isArray(entry.args) ? entry.args : [],
|
|
315
|
+
cwd: entry.cwd || process.cwd(),
|
|
316
|
+
env,
|
|
317
|
+
stderr: 'pipe',
|
|
318
|
+
});
|
|
319
|
+
await client.connect(transport);
|
|
320
|
+
const tools = await listAllMcpTools(client);
|
|
321
|
+
return { serverName, client, transport, tools };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (entry.url) {
|
|
325
|
+
const urlText = normalizeText(entry.url);
|
|
326
|
+
if (!urlText) return null;
|
|
327
|
+
const parsed = new URL(urlText);
|
|
328
|
+
if (parsed.protocol === 'ws:' || parsed.protocol === 'wss:') {
|
|
329
|
+
const client = new Client({ name: 'sandbox', version: '0.1.0' });
|
|
330
|
+
const transport = new WebSocketClientTransport(parsed);
|
|
331
|
+
await client.connect(transport);
|
|
332
|
+
const tools = await listAllMcpTools(client);
|
|
333
|
+
return { serverName, client, transport, tools };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const errors = [];
|
|
86
337
|
try {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
338
|
+
const client = new Client({ name: 'sandbox', version: '0.1.0' });
|
|
339
|
+
const transport = new StreamableHTTPClientTransport(parsed);
|
|
340
|
+
await client.connect(transport);
|
|
341
|
+
const tools = await listAllMcpTools(client);
|
|
342
|
+
return { serverName, client, transport, tools };
|
|
343
|
+
} catch (err) {
|
|
344
|
+
errors.push(`streamable_http: ${err?.message || err}`);
|
|
90
345
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
346
|
+
try {
|
|
347
|
+
const client = new Client({ name: 'sandbox', version: '0.1.0' });
|
|
348
|
+
const transport = new SSEClientTransport(parsed);
|
|
349
|
+
await client.connect(transport);
|
|
350
|
+
const tools = await listAllMcpTools(client);
|
|
351
|
+
return { serverName, client, transport, tools };
|
|
352
|
+
} catch (err) {
|
|
353
|
+
errors.push(`sse: ${err?.message || err}`);
|
|
96
354
|
}
|
|
97
|
-
|
|
355
|
+
throw new Error(`Failed to connect MCP server (${serverName}): ${errors.join(' | ')}`);
|
|
356
|
+
}
|
|
98
357
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function buildAppMcpEntry({ pluginDir, pluginId, app }) {
|
|
362
|
+
const mcp = app?.ai?.mcp && typeof app.ai.mcp === 'object' ? app.ai.mcp : null;
|
|
363
|
+
if (!mcp) return null;
|
|
364
|
+
if (mcp.enabled === false) return null;
|
|
365
|
+
const serverName = mcp?.name ? String(mcp.name).trim() : `${pluginId}.${app.id}`;
|
|
366
|
+
const command = normalizeText(mcp.command) || 'node';
|
|
367
|
+
const args = Array.isArray(mcp.args) ? mcp.args : [];
|
|
368
|
+
const entryRel = normalizeText(mcp.entry);
|
|
369
|
+
if (entryRel) {
|
|
370
|
+
const entryAbs = resolveInsideDir(pluginDir, entryRel);
|
|
371
|
+
return { name: serverName, command, args: [entryAbs, ...args], cwd: pluginDir };
|
|
372
|
+
}
|
|
373
|
+
const urlText = normalizeText(mcp.url);
|
|
374
|
+
if (urlText) {
|
|
375
|
+
return { name: serverName, url: urlText };
|
|
376
|
+
}
|
|
377
|
+
if (normalizeText(mcp.command)) {
|
|
378
|
+
return { name: serverName, command, args, cwd: pluginDir };
|
|
379
|
+
}
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function readPromptSource(source, pluginDir) {
|
|
384
|
+
if (!source) return '';
|
|
385
|
+
if (typeof source === 'string') {
|
|
386
|
+
const rel = source.trim();
|
|
387
|
+
if (!rel) return '';
|
|
388
|
+
const abs = resolveInsideDir(pluginDir, rel);
|
|
389
|
+
if (!isFile(abs)) return '';
|
|
390
|
+
return fs.readFileSync(abs, 'utf8');
|
|
391
|
+
}
|
|
392
|
+
if (typeof source === 'object') {
|
|
393
|
+
const content = normalizeText(source?.content);
|
|
394
|
+
if (content) return content;
|
|
395
|
+
const rel = normalizeText(source?.path);
|
|
396
|
+
if (!rel) return '';
|
|
397
|
+
const abs = resolveInsideDir(pluginDir, rel);
|
|
398
|
+
if (!isFile(abs)) return '';
|
|
399
|
+
return fs.readFileSync(abs, 'utf8');
|
|
400
|
+
}
|
|
401
|
+
return '';
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function resolveAppMcpPrompt(app, pluginDir) {
|
|
405
|
+
const prompt = app?.ai?.mcpPrompt;
|
|
406
|
+
if (!prompt) return '';
|
|
407
|
+
if (typeof prompt === 'string') {
|
|
408
|
+
return readPromptSource(prompt, pluginDir);
|
|
409
|
+
}
|
|
410
|
+
if (typeof prompt === 'object') {
|
|
411
|
+
const zh = readPromptSource(prompt.zh, pluginDir);
|
|
412
|
+
const en = readPromptSource(prompt.en, pluginDir);
|
|
413
|
+
return zh || en || '';
|
|
414
|
+
}
|
|
415
|
+
return '';
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async function callOpenAiChat({ apiKey, baseUrl, model, messages, tools, signal }) {
|
|
419
|
+
const endpoint = resolveChatCompletionsUrl(baseUrl);
|
|
420
|
+
const payload = {
|
|
421
|
+
model,
|
|
422
|
+
messages,
|
|
423
|
+
stream: false,
|
|
106
424
|
};
|
|
425
|
+
if (Array.isArray(tools) && tools.length > 0) {
|
|
426
|
+
payload.tools = tools;
|
|
427
|
+
payload.tool_choice = 'auto';
|
|
428
|
+
}
|
|
429
|
+
const res = await fetch(endpoint, {
|
|
430
|
+
method: 'POST',
|
|
431
|
+
headers: {
|
|
432
|
+
'content-type': 'application/json',
|
|
433
|
+
authorization: `Bearer ${apiKey}`,
|
|
434
|
+
},
|
|
435
|
+
body: JSON.stringify(payload),
|
|
436
|
+
signal,
|
|
437
|
+
});
|
|
438
|
+
if (!res.ok) {
|
|
439
|
+
const text = await res.text();
|
|
440
|
+
throw new Error(`LLM request failed (${res.status}): ${text || res.statusText}`);
|
|
441
|
+
}
|
|
442
|
+
return await res.json();
|
|
443
|
+
}
|
|
107
444
|
|
|
108
|
-
|
|
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
|
+
|
|
454
|
+
function sendText(res, status, text, contentType) {
|
|
455
|
+
res.writeHead(status, {
|
|
456
|
+
'content-type': contentType || 'text/plain; charset=utf-8',
|
|
457
|
+
'cache-control': 'no-store',
|
|
458
|
+
});
|
|
459
|
+
res.end(text);
|
|
460
|
+
}
|
|
109
461
|
|
|
110
|
-
|
|
111
|
-
|
|
462
|
+
function readJsonBody(req) {
|
|
463
|
+
return new Promise((resolve, reject) => {
|
|
464
|
+
let body = '';
|
|
465
|
+
req.on('data', (chunk) => {
|
|
466
|
+
body += chunk;
|
|
467
|
+
});
|
|
468
|
+
req.on('end', () => {
|
|
469
|
+
if (!body) return resolve({});
|
|
112
470
|
try {
|
|
113
|
-
|
|
114
|
-
} catch {
|
|
115
|
-
|
|
471
|
+
resolve(JSON.parse(body));
|
|
472
|
+
} catch (err) {
|
|
473
|
+
reject(err);
|
|
116
474
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
for (const w of watchers.values()) {
|
|
120
|
-
try {
|
|
121
|
-
w.close();
|
|
122
|
-
} catch {
|
|
123
|
-
// ignore
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
watchers.clear();
|
|
127
|
-
};
|
|
475
|
+
});
|
|
476
|
+
});
|
|
128
477
|
}
|
|
129
478
|
|
|
130
|
-
function
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
479
|
+
function guessContentType(filePath) {
|
|
480
|
+
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
|
+
}
|
|
701
|
+
#sandboxInspectorBody {
|
|
702
|
+
padding: 10px 12px;
|
|
703
|
+
overflow: auto;
|
|
704
|
+
display: flex;
|
|
705
|
+
flex-direction: column;
|
|
706
|
+
gap: 10px;
|
|
155
707
|
}
|
|
156
|
-
#
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
708
|
+
#llmPanel {
|
|
709
|
+
position: fixed;
|
|
710
|
+
right: 12px;
|
|
711
|
+
top: 72px;
|
|
712
|
+
width: 420px;
|
|
713
|
+
max-height: 70vh;
|
|
714
|
+
display: none;
|
|
715
|
+
flex-direction: column;
|
|
716
|
+
background: var(--ds-panel-bg);
|
|
717
|
+
border: 1px solid var(--ds-panel-border);
|
|
718
|
+
border-radius: 12px;
|
|
719
|
+
overflow: hidden;
|
|
720
|
+
box-shadow: 0 14px 40px rgba(0,0,0,0.16);
|
|
721
|
+
z-index: 11;
|
|
722
|
+
}
|
|
723
|
+
#llmPanelHeader {
|
|
724
|
+
padding: 10px 12px;
|
|
725
|
+
display:flex;
|
|
726
|
+
align-items:center;
|
|
727
|
+
justify-content: space-between;
|
|
728
|
+
border-bottom: 1px solid var(--ds-panel-border);
|
|
729
|
+
}
|
|
730
|
+
#llmPanelBody {
|
|
731
|
+
padding: 10px 12px;
|
|
732
|
+
overflow: auto;
|
|
733
|
+
display: flex;
|
|
734
|
+
flex-direction: column;
|
|
735
|
+
gap: 10px;
|
|
736
|
+
}
|
|
737
|
+
.section-title { font-size: 12px; font-weight: 700; opacity: 0.8; }
|
|
738
|
+
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 12px; white-space: pre-wrap; }
|
|
739
|
+
input, textarea, select {
|
|
740
|
+
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>
|
|
767
|
+
<div id="themeStatus" class="muted"></div>
|
|
768
|
+
<div id="sandboxContext" class="muted"></div>
|
|
769
|
+
<button id="btnLlmConfig" class="btn" type="button">AI Config</button>
|
|
770
|
+
<button id="btnInspectorToggle" class="btn" type="button">Inspect</button>
|
|
176
771
|
<button id="btnReload" class="btn" type="button">Reload</button>
|
|
177
772
|
</div>
|
|
178
773
|
</div>
|
|
179
774
|
</div>
|
|
180
|
-
<div id="headerSlot"></div>
|
|
181
|
-
<div id="container"><div id="containerInner"></div></div>
|
|
182
|
-
</div>
|
|
183
|
-
|
|
184
|
-
<button id="promptsFab" class="btn" type="button">:)</button>
|
|
185
|
-
|
|
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
|
+
|
|
186
781
|
<div id="promptsPanel">
|
|
187
782
|
<div id="promptsPanelHeader">
|
|
188
783
|
<div style="font-weight:800">UI Prompts</div>
|
|
@@ -191,393 +786,785 @@ function htmlPage() {
|
|
|
191
786
|
<div id="promptsPanelBody"></div>
|
|
192
787
|
</div>
|
|
193
788
|
|
|
194
|
-
<
|
|
195
|
-
|
|
196
|
-
</
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
789
|
+
<div id="llmPanel" aria-hidden="true">
|
|
790
|
+
<div id="llmPanelHeader">
|
|
791
|
+
<div style="font-weight:800">Sandbox LLM</div>
|
|
792
|
+
<div class="row">
|
|
793
|
+
<button id="btnLlmRefresh" class="btn" type="button">Refresh</button>
|
|
794
|
+
<button id="btnLlmClose" class="btn" type="button">Close</button>
|
|
795
|
+
</div>
|
|
796
|
+
</div>
|
|
797
|
+
<div id="llmPanelBody">
|
|
798
|
+
<div class="card">
|
|
799
|
+
<label for="llmApiKey">API Key</label>
|
|
800
|
+
<input id="llmApiKey" type="password" placeholder="sk-..." autocomplete="off" />
|
|
801
|
+
<div id="llmKeyStatus" class="muted"></div>
|
|
802
|
+
</div>
|
|
803
|
+
<div class="card">
|
|
804
|
+
<label for="llmBaseUrl">Base URL</label>
|
|
805
|
+
<input id="llmBaseUrl" type="text" placeholder="https://api.openai.com/v1" />
|
|
806
|
+
</div>
|
|
807
|
+
<div class="card">
|
|
808
|
+
<label for="llmModelId">Model ID</label>
|
|
809
|
+
<input id="llmModelId" type="text" placeholder="gpt-4o-mini" />
|
|
810
|
+
</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
|
+
<div class="row">
|
|
817
|
+
<button id="btnLlmSave" class="btn" type="button">Save</button>
|
|
818
|
+
<button id="btnLlmClear" class="btn" type="button">Clear Key</button>
|
|
819
|
+
</div>
|
|
820
|
+
<div id="llmStatus" class="muted"></div>
|
|
821
|
+
</div>
|
|
822
|
+
</div>
|
|
201
823
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
824
|
+
<div id="sandboxInspector" aria-hidden="true">
|
|
825
|
+
<div id="sandboxInspectorHeader">
|
|
826
|
+
<div style="font-weight:800">Sandbox Inspector</div>
|
|
827
|
+
<div class="row">
|
|
828
|
+
<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');
|
|
869
|
+
const btnInspectorClose = $('#btnInspectorClose');
|
|
870
|
+
const btnInspectorRefresh = $('#btnInspectorRefresh');
|
|
871
|
+
const inspectorContext = $('#inspectorContext');
|
|
872
|
+
const inspectorTheme = $('#inspectorTheme');
|
|
873
|
+
const inspectorTokens = $('#inspectorTokens');
|
|
874
|
+
const btnLlmConfig = $('#btnLlmConfig');
|
|
875
|
+
const llmPanel = $('#llmPanel');
|
|
876
|
+
const btnLlmClose = $('#btnLlmClose');
|
|
877
|
+
const btnLlmRefresh = $('#btnLlmRefresh');
|
|
878
|
+
const btnLlmSave = $('#btnLlmSave');
|
|
879
|
+
const btnLlmClear = $('#btnLlmClear');
|
|
880
|
+
const llmApiKey = $('#llmApiKey');
|
|
881
|
+
const llmBaseUrl = $('#llmBaseUrl');
|
|
882
|
+
const llmModelId = $('#llmModelId');
|
|
883
|
+
const llmWorkdir = $('#llmWorkdir');
|
|
884
|
+
const llmStatus = $('#llmStatus');
|
|
885
|
+
const llmKeyStatus = $('#llmKeyStatus');
|
|
208
886
|
|
|
209
887
|
const setPanelOpen = (open) => { panel.style.display = open ? 'flex' : 'none'; };
|
|
210
888
|
fab.addEventListener('click', () => setPanelOpen(panel.style.display !== 'flex'));
|
|
211
889
|
panelClose.addEventListener('click', () => setPanelOpen(false));
|
|
212
890
|
window.addEventListener('chatos:uiPrompts:open', () => setPanelOpen(true));
|
|
213
|
-
window.addEventListener('chatos:uiPrompts:close', () => setPanelOpen(false));
|
|
214
|
-
window.addEventListener('chatos:uiPrompts:toggle', () => setPanelOpen(panel.style.display !== 'flex'));
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
const
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
+
|
|
939
|
+
const updateContextStatus = () => {
|
|
940
|
+
if (!sandboxContext) return;
|
|
941
|
+
sandboxContext.textContent = __SANDBOX__.pluginId + ':' + __SANDBOX__.appId;
|
|
222
942
|
};
|
|
223
943
|
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
function renderPrompts() {
|
|
227
|
-
panelBody.textContent = '';
|
|
228
|
-
const pending = new Map();
|
|
229
|
-
for (const e of entries) {
|
|
230
|
-
if (e?.type !== 'ui_prompt') continue;
|
|
231
|
-
const id = String(e?.requestId || '');
|
|
232
|
-
if (!id) continue;
|
|
233
|
-
if (e.action === 'request') pending.set(id, e);
|
|
234
|
-
if (e.action === 'response') pending.delete(id);
|
|
235
|
-
}
|
|
944
|
+
const isInspectorOpen = () => sandboxInspector && sandboxInspector.style.display === 'flex';
|
|
945
|
+
const isLlmPanelOpen = () => llmPanel && llmPanel.style.display === 'flex';
|
|
236
946
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
947
|
+
const setLlmStatus = (text, isError) => {
|
|
948
|
+
if (!llmStatus) return;
|
|
949
|
+
llmStatus.textContent = text || '';
|
|
950
|
+
llmStatus.style.color = isError ? '#ef4444' : '';
|
|
951
|
+
};
|
|
244
952
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
source.textContent = req?.prompt?.source ? String(req.prompt.source) : '';
|
|
262
|
-
|
|
263
|
-
const form = document.createElement('div');
|
|
264
|
-
form.style.marginTop = '10px';
|
|
265
|
-
form.style.display = 'grid';
|
|
266
|
-
form.style.gap = '10px';
|
|
267
|
-
|
|
268
|
-
const kind = String(req?.prompt?.kind || '');
|
|
269
|
-
|
|
270
|
-
const mkBtn = (label, danger) => {
|
|
271
|
-
const btn = document.createElement('button');
|
|
272
|
-
btn.type = 'button';
|
|
273
|
-
btn.className = 'btn' + (danger ? ' danger' : '');
|
|
274
|
-
btn.textContent = label;
|
|
275
|
-
return btn;
|
|
276
|
-
};
|
|
953
|
+
const refreshLlmConfig = async () => {
|
|
954
|
+
try {
|
|
955
|
+
setLlmStatus('Loading...');
|
|
956
|
+
const r = await fetch('/api/sandbox/llm-config');
|
|
957
|
+
const j = await r.json();
|
|
958
|
+
if (!j?.ok) throw new Error(j?.message || 'Failed to load config');
|
|
959
|
+
const cfg = j?.config || {};
|
|
960
|
+
if (llmBaseUrl) llmBaseUrl.value = cfg.baseUrl || '';
|
|
961
|
+
if (llmModelId) llmModelId.value = cfg.modelId || '';
|
|
962
|
+
if (llmWorkdir) llmWorkdir.value = cfg.workdir || '';
|
|
963
|
+
if (llmKeyStatus) llmKeyStatus.textContent = cfg.hasApiKey ? 'API key set' : 'API key missing';
|
|
964
|
+
setLlmStatus('');
|
|
965
|
+
} catch (err) {
|
|
966
|
+
setLlmStatus(err?.message || String(err), true);
|
|
967
|
+
}
|
|
968
|
+
};
|
|
277
969
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
970
|
+
const saveLlmConfig = async ({ clearKey } = {}) => {
|
|
971
|
+
try {
|
|
972
|
+
setLlmStatus('Saving...');
|
|
973
|
+
const payload = {
|
|
974
|
+
baseUrl: llmBaseUrl ? llmBaseUrl.value : '',
|
|
975
|
+
modelId: llmModelId ? llmModelId.value : '',
|
|
976
|
+
workdir: llmWorkdir ? llmWorkdir.value : '',
|
|
281
977
|
};
|
|
282
|
-
|
|
283
|
-
if (
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const key = String(f?.key || '');
|
|
288
|
-
if (!key) continue;
|
|
289
|
-
const wrap = document.createElement('div');
|
|
290
|
-
const lab = document.createElement('label');
|
|
291
|
-
lab.textContent = f?.label ? String(f.label) : key;
|
|
292
|
-
const input = document.createElement(f?.multiline ? 'textarea' : 'input');
|
|
293
|
-
input.placeholder = f?.placeholder ? String(f.placeholder) : '';
|
|
294
|
-
input.value = f?.default ? String(f.default) : '';
|
|
295
|
-
input.addEventListener('input', () => { values[key] = String(input.value || ''); });
|
|
296
|
-
values[key] = String(input.value || '');
|
|
297
|
-
wrap.appendChild(lab);
|
|
298
|
-
wrap.appendChild(input);
|
|
299
|
-
form.appendChild(wrap);
|
|
300
|
-
}
|
|
301
|
-
const row = document.createElement('div');
|
|
302
|
-
row.className = 'row';
|
|
303
|
-
const ok = mkBtn('Submit');
|
|
304
|
-
ok.addEventListener('click', () => submit({ status: 'ok', values }));
|
|
305
|
-
const cancel = mkBtn('Cancel', true);
|
|
306
|
-
cancel.addEventListener('click', () => submit({ status: 'cancel' }));
|
|
307
|
-
row.appendChild(ok);
|
|
308
|
-
row.appendChild(cancel);
|
|
309
|
-
form.appendChild(row);
|
|
310
|
-
} else if (kind === 'choice') {
|
|
311
|
-
const options = Array.isArray(req?.prompt?.options) ? req.prompt.options : [];
|
|
312
|
-
const multiple = Boolean(req?.prompt?.multiple);
|
|
313
|
-
const selected = new Set();
|
|
314
|
-
const wrap = document.createElement('div');
|
|
315
|
-
const lab = document.createElement('label');
|
|
316
|
-
lab.textContent = '选择';
|
|
317
|
-
const select = document.createElement('select');
|
|
318
|
-
if (multiple) select.multiple = true;
|
|
319
|
-
for (const opt of options) {
|
|
320
|
-
const v = String(opt?.value || '');
|
|
321
|
-
const o = document.createElement('option');
|
|
322
|
-
o.value = v;
|
|
323
|
-
o.textContent = opt?.label ? String(opt.label) : v;
|
|
324
|
-
select.appendChild(o);
|
|
325
|
-
}
|
|
326
|
-
select.addEventListener('change', () => {
|
|
327
|
-
selected.clear();
|
|
328
|
-
for (const o of select.selectedOptions) selected.add(String(o.value));
|
|
329
|
-
});
|
|
330
|
-
wrap.appendChild(lab);
|
|
331
|
-
wrap.appendChild(select);
|
|
332
|
-
form.appendChild(wrap);
|
|
333
|
-
const row = document.createElement('div');
|
|
334
|
-
row.className = 'row';
|
|
335
|
-
const ok = mkBtn('Submit');
|
|
336
|
-
ok.addEventListener('click', () => submit({ status: 'ok', value: multiple ? Array.from(selected) : Array.from(selected)[0] || '' }));
|
|
337
|
-
const cancel = mkBtn('Cancel', true);
|
|
338
|
-
cancel.addEventListener('click', () => submit({ status: 'cancel' }));
|
|
339
|
-
row.appendChild(ok);
|
|
340
|
-
row.appendChild(cancel);
|
|
341
|
-
form.appendChild(row);
|
|
342
|
-
} else {
|
|
343
|
-
const row = document.createElement('div');
|
|
344
|
-
row.className = 'row';
|
|
345
|
-
const ok = mkBtn('OK');
|
|
346
|
-
ok.addEventListener('click', () => submit({ status: 'ok' }));
|
|
347
|
-
const cancel = mkBtn('Cancel', true);
|
|
348
|
-
cancel.addEventListener('click', () => submit({ status: 'cancel' }));
|
|
349
|
-
row.appendChild(ok);
|
|
350
|
-
row.appendChild(cancel);
|
|
351
|
-
form.appendChild(row);
|
|
978
|
+
const apiKey = llmApiKey ? llmApiKey.value : '';
|
|
979
|
+
if (clearKey) {
|
|
980
|
+
payload.apiKey = '';
|
|
981
|
+
} else if (apiKey && apiKey.trim()) {
|
|
982
|
+
payload.apiKey = apiKey.trim();
|
|
352
983
|
}
|
|
984
|
+
const r = await fetch('/api/sandbox/llm-config', {
|
|
985
|
+
method: 'POST',
|
|
986
|
+
headers: { 'content-type': 'application/json' },
|
|
987
|
+
body: JSON.stringify(payload),
|
|
988
|
+
});
|
|
989
|
+
const j = await r.json();
|
|
990
|
+
if (!j?.ok) throw new Error(j?.message || 'Failed to save config');
|
|
991
|
+
if (llmApiKey) llmApiKey.value = '';
|
|
992
|
+
await refreshLlmConfig();
|
|
993
|
+
setLlmStatus('Saved');
|
|
994
|
+
} catch (err) {
|
|
995
|
+
setLlmStatus(err?.message || String(err), true);
|
|
996
|
+
}
|
|
997
|
+
};
|
|
998
|
+
|
|
999
|
+
const setLlmPanelOpen = (open) => {
|
|
1000
|
+
if (!llmPanel) return;
|
|
1001
|
+
llmPanel.style.display = open ? 'flex' : 'none';
|
|
1002
|
+
llmPanel.setAttribute('aria-hidden', open ? 'false' : 'true');
|
|
1003
|
+
if (open) refreshLlmConfig();
|
|
1004
|
+
};
|
|
353
1005
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
1006
|
+
const formatJson = (value) => {
|
|
1007
|
+
try {
|
|
1008
|
+
return JSON.stringify(value, null, 2);
|
|
1009
|
+
} catch {
|
|
1010
|
+
return String(value);
|
|
1011
|
+
}
|
|
1012
|
+
};
|
|
1013
|
+
|
|
1014
|
+
const tokenNameList = Array.isArray(__SANDBOX__.tokenNames) ? __SANDBOX__.tokenNames : [];
|
|
1015
|
+
const sandboxContextBase = __SANDBOX__.context || { pluginId: __SANDBOX__.pluginId, appId: __SANDBOX__.appId };
|
|
1016
|
+
|
|
1017
|
+
const collectTokens = () => {
|
|
1018
|
+
const style = getComputedStyle(document.documentElement);
|
|
1019
|
+
const names = new Set(tokenNameList);
|
|
1020
|
+
for (let i = 0; i < style.length; i += 1) {
|
|
1021
|
+
const name = style[i];
|
|
1022
|
+
if (name && name.startsWith('--ds-')) names.add(name);
|
|
1023
|
+
}
|
|
1024
|
+
return [...names]
|
|
1025
|
+
.sort()
|
|
1026
|
+
.map((name) => {
|
|
1027
|
+
const value = style.getPropertyValue(name).trim();
|
|
1028
|
+
return name + ': ' + (value || '(unset)');
|
|
1029
|
+
})
|
|
1030
|
+
.join('\\n');
|
|
1031
|
+
};
|
|
1032
|
+
|
|
1033
|
+
const readHostContext = () => {
|
|
1034
|
+
if (!inspectorEnabled) return null;
|
|
1035
|
+
if (typeof host?.context?.get === 'function') return host.context.get();
|
|
1036
|
+
return { ...sandboxContextBase, theme: currentTheme, bridge: { enabled: true } };
|
|
1037
|
+
};
|
|
1038
|
+
|
|
1039
|
+
const readThemeInfo = () => ({
|
|
1040
|
+
themeMode,
|
|
1041
|
+
currentTheme,
|
|
1042
|
+
dataTheme: document.documentElement.dataset.theme || '',
|
|
1043
|
+
dataThemeMode: document.documentElement.dataset.themeMode || '',
|
|
1044
|
+
prefersColorScheme: systemQuery ? (systemQuery.matches ? 'dark' : 'light') : 'unknown',
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
const updateInspector = () => {
|
|
1048
|
+
if (!inspectorEnabled) return;
|
|
1049
|
+
if (inspectorContext) inspectorContext.textContent = formatJson(readHostContext());
|
|
1050
|
+
if (inspectorTheme) inspectorTheme.textContent = formatJson(readThemeInfo());
|
|
1051
|
+
if (inspectorTokens) inspectorTokens.textContent = collectTokens();
|
|
1052
|
+
};
|
|
1053
|
+
|
|
1054
|
+
const startInspectorTimer = () => {
|
|
1055
|
+
if (inspectorTimer) return;
|
|
1056
|
+
inspectorTimer = setInterval(updateInspector, 1000);
|
|
1057
|
+
};
|
|
1058
|
+
|
|
1059
|
+
const stopInspectorTimer = () => {
|
|
1060
|
+
if (!inspectorTimer) return;
|
|
1061
|
+
clearInterval(inspectorTimer);
|
|
1062
|
+
inspectorTimer = null;
|
|
1063
|
+
};
|
|
1064
|
+
|
|
1065
|
+
const setInspectorOpen = (open) => {
|
|
1066
|
+
if (!sandboxInspector) return;
|
|
1067
|
+
sandboxInspector.style.display = open ? 'flex' : 'none';
|
|
1068
|
+
sandboxInspector.setAttribute('aria-hidden', open ? 'false' : 'true');
|
|
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
|
+
|
|
1107
|
+
if (btnThemeLight) btnThemeLight.addEventListener('click', () => applyThemeMode('light'));
|
|
1108
|
+
if (btnThemeDark) btnThemeDark.addEventListener('click', () => applyThemeMode('dark'));
|
|
1109
|
+
if (btnThemeSystem) btnThemeSystem.addEventListener('click', () => applyThemeMode('system'));
|
|
1110
|
+
if (btnLlmConfig) btnLlmConfig.addEventListener('click', () => setLlmPanelOpen(!isLlmPanelOpen()));
|
|
1111
|
+
if (btnLlmClose) btnLlmClose.addEventListener('click', () => setLlmPanelOpen(false));
|
|
1112
|
+
if (btnLlmRefresh) btnLlmRefresh.addEventListener('click', () => refreshLlmConfig());
|
|
1113
|
+
if (btnLlmSave) btnLlmSave.addEventListener('click', () => saveLlmConfig());
|
|
1114
|
+
if (btnLlmClear) btnLlmClear.addEventListener('click', () => saveLlmConfig({ clearKey: true }));
|
|
1115
|
+
if (btnInspectorToggle) btnInspectorToggle.addEventListener('click', () => setInspectorOpen(!isInspectorOpen()));
|
|
1116
|
+
if (btnInspectorClose) btnInspectorClose.addEventListener('click', () => setInspectorOpen(false));
|
|
1117
|
+
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);
|
|
359
1265
|
}
|
|
360
1266
|
}
|
|
361
1267
|
|
|
362
|
-
const
|
|
1268
|
+
const buildChatMessages = (list) => {
|
|
1269
|
+
const out = [];
|
|
1270
|
+
if (!Array.isArray(list)) return out;
|
|
1271
|
+
for (const msg of list) {
|
|
1272
|
+
const role = String(msg?.role || '').trim();
|
|
1273
|
+
const text = typeof msg?.text === 'string' ? msg.text : '';
|
|
1274
|
+
if (!text || !text.trim()) continue;
|
|
1275
|
+
if (role !== 'user' && role !== 'assistant' && role !== 'system') continue;
|
|
1276
|
+
out.push({ role, text });
|
|
1277
|
+
}
|
|
1278
|
+
return out;
|
|
1279
|
+
};
|
|
363
1280
|
|
|
1281
|
+
const callSandboxChat = async (payload, signal) => {
|
|
1282
|
+
const r = await fetch('/api/llm/chat', {
|
|
1283
|
+
method: 'POST',
|
|
1284
|
+
headers: { 'content-type': 'application/json' },
|
|
1285
|
+
body: JSON.stringify(payload || {}),
|
|
1286
|
+
signal,
|
|
1287
|
+
});
|
|
1288
|
+
const j = await r.json();
|
|
1289
|
+
if (!j?.ok) throw new Error(j?.message || 'Sandbox LLM call failed');
|
|
1290
|
+
return j;
|
|
1291
|
+
};
|
|
1292
|
+
|
|
1293
|
+
const getTheme = () => currentTheme || resolveTheme();
|
|
1294
|
+
|
|
364
1295
|
const host = {
|
|
365
1296
|
bridge: { enabled: true },
|
|
366
|
-
context: { get: () => ({
|
|
1297
|
+
context: { get: () => ({ ...sandboxContextBase, theme: getTheme(), bridge: { enabled: true } }) },
|
|
367
1298
|
theme: {
|
|
368
|
-
get: getTheme,
|
|
369
|
-
onChange: (listener) => {
|
|
370
|
-
if (
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
},
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
list: async () => ({ ok: true, agents: clone(agents) }),
|
|
478
|
-
ensureDefault: async () => ({ ok: true, agent: clone(await ensureAgent()) }),
|
|
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()) }),
|
|
479
1408
|
create: async (payload) => {
|
|
480
1409
|
const agent = {
|
|
481
1410
|
id: 'sandbox-agent-' + uuid(),
|
|
482
1411
|
name: payload?.name ? String(payload.name) : 'Sandbox Agent',
|
|
483
1412
|
description: payload?.description ? String(payload.description) : '',
|
|
1413
|
+
modelId: payload?.modelId ? String(payload.modelId) : '',
|
|
484
1414
|
};
|
|
485
1415
|
agents.unshift(agent);
|
|
486
1416
|
return { ok: true, agent: clone(agent) };
|
|
487
1417
|
},
|
|
488
1418
|
update: async (id, patch) => {
|
|
489
|
-
const agentId = String(id || '').trim();
|
|
490
|
-
if (!agentId) throw new Error('id is required');
|
|
1419
|
+
const agentId = String(id || '').trim();
|
|
1420
|
+
if (!agentId) throw new Error('id is required');
|
|
491
1421
|
const idx = agents.findIndex((a) => a.id === agentId);
|
|
492
1422
|
if (idx < 0) throw new Error('agent not found');
|
|
493
1423
|
const a = agents[idx];
|
|
494
1424
|
if (patch?.name) a.name = String(patch.name);
|
|
495
1425
|
if (patch?.description) a.description = String(patch.description);
|
|
1426
|
+
if (patch?.modelId) a.modelId = String(patch.modelId);
|
|
496
1427
|
return { ok: true, agent: clone(a) };
|
|
497
1428
|
},
|
|
498
|
-
delete: async (id) => {
|
|
499
|
-
const agentId = String(id || '').trim();
|
|
500
|
-
if (!agentId) throw new Error('id is required');
|
|
501
|
-
const idx = agents.findIndex((a) => a.id === agentId);
|
|
502
|
-
if (idx < 0) return { ok: true, deleted: false };
|
|
503
|
-
agents.splice(idx, 1);
|
|
504
|
-
return { ok: true, deleted: true };
|
|
505
|
-
},
|
|
506
|
-
createForApp: async (payload) => {
|
|
507
|
-
const name = payload?.name ? String(payload.name) : 'App Agent (' + __SANDBOX__.appId + ')';
|
|
508
|
-
return await agentsApi.create({ ...payload, name });
|
|
509
|
-
},
|
|
510
|
-
};
|
|
511
|
-
|
|
512
|
-
const sessionsApi = {
|
|
513
|
-
list: async () => ({ ok: true, sessions: clone(Array.from(sessions.values())) }),
|
|
514
|
-
ensureDefault: async (payload) => {
|
|
515
|
-
const session = await ensureSession(payload?.agentId);
|
|
516
|
-
return { ok: true, session: clone(session) };
|
|
517
|
-
},
|
|
518
|
-
create: async (payload) => {
|
|
519
|
-
const agentId = payload?.agentId ? String(payload.agentId) : (await ensureAgent()).id;
|
|
520
|
-
const id = 'sandbox-session-' + uuid();
|
|
521
|
-
const session = { id, agentId, createdAt: new Date().toISOString() };
|
|
522
|
-
sessions.set(id, session);
|
|
523
|
-
if (!messagesBySession.has(id)) messagesBySession.set(id, []);
|
|
524
|
-
return { ok: true, session: clone(session) };
|
|
525
|
-
},
|
|
526
|
-
};
|
|
527
|
-
|
|
528
|
-
const messagesApi = {
|
|
529
|
-
list: async (payload) => {
|
|
530
|
-
const sessionId = String(payload?.sessionId || '').trim();
|
|
531
|
-
if (!sessionId) throw new Error('sessionId is required');
|
|
532
|
-
const msgs = messagesBySession.get(sessionId) || [];
|
|
533
|
-
return { ok: true, messages: clone(msgs) };
|
|
534
|
-
},
|
|
535
|
-
};
|
|
536
|
-
|
|
537
|
-
const abort = async (payload) => {
|
|
538
|
-
const sessionId = String(payload?.sessionId || '').trim();
|
|
539
|
-
if (!sessionId) throw new Error('sessionId is required');
|
|
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');
|
|
540
1471
|
const run = activeRuns.get(sessionId);
|
|
541
1472
|
if (run) {
|
|
542
1473
|
run.aborted = true;
|
|
543
|
-
|
|
1474
|
+
if (run.controller) {
|
|
544
1475
|
try {
|
|
545
|
-
|
|
1476
|
+
run.controller.abort();
|
|
546
1477
|
} catch {
|
|
547
1478
|
// ignore
|
|
548
1479
|
}
|
|
549
1480
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
1481
|
+
for (const t of run.timers) {
|
|
1482
|
+
try {
|
|
1483
|
+
clearTimeout(t);
|
|
1484
|
+
} catch {
|
|
1485
|
+
// ignore
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
activeRuns.delete(sessionId);
|
|
1489
|
+
}
|
|
1490
|
+
emit({ type: 'assistant_abort', sessionId, ts: new Date().toISOString() });
|
|
1491
|
+
return { ok: true };
|
|
1492
|
+
};
|
|
1493
|
+
|
|
556
1494
|
const send = async (payload) => {
|
|
557
1495
|
const sessionId = String(payload?.sessionId || '').trim();
|
|
558
1496
|
const text = String(payload?.text || '').trim();
|
|
559
1497
|
if (!sessionId) throw new Error('sessionId is required');
|
|
560
1498
|
if (!text) throw new Error('text is required');
|
|
561
|
-
|
|
562
|
-
if (!sessions.has(sessionId)) throw new Error('session not found');
|
|
563
|
-
|
|
564
|
-
const msgs = messagesBySession.get(sessionId) || [];
|
|
565
|
-
const userMsg = { id: 'msg-' + uuid(), role: 'user', text, ts: new Date().toISOString() };
|
|
566
|
-
msgs.push(userMsg);
|
|
567
|
-
messagesBySession.set(sessionId, msgs);
|
|
568
|
-
emit({ type: 'user_message', sessionId, message: clone(userMsg) });
|
|
569
|
-
|
|
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
|
+
|
|
570
1508
|
const assistantMsg = { id: 'msg-' + uuid(), role: 'assistant', text: '', ts: new Date().toISOString() };
|
|
571
1509
|
msgs.push(assistantMsg);
|
|
572
1510
|
emit({ type: 'assistant_start', sessionId, message: clone(assistantMsg) });
|
|
573
1511
|
|
|
574
|
-
const
|
|
575
|
-
const chunks = [];
|
|
576
|
-
for (let i = 0; i < out.length; i += 8) chunks.push(out.slice(i, i + 8));
|
|
577
|
-
|
|
578
|
-
const run = { aborted: false, timers: [] };
|
|
1512
|
+
const run = { aborted: false, timers: [], controller: new AbortController() };
|
|
579
1513
|
activeRuns.set(sessionId, run);
|
|
580
1514
|
|
|
1515
|
+
let result = null;
|
|
1516
|
+
try {
|
|
1517
|
+
const session = sessions.get(sessionId);
|
|
1518
|
+
const agent = session ? agents.find((a) => a.id === session.agentId) : null;
|
|
1519
|
+
const agentModelId = agent?.modelId ? String(agent.modelId) : '';
|
|
1520
|
+
const chatPayload = {
|
|
1521
|
+
messages: buildChatMessages(msgs),
|
|
1522
|
+
modelId: typeof payload?.modelId === 'string' && payload.modelId.trim() ? payload.modelId : agentModelId,
|
|
1523
|
+
modelName: typeof payload?.modelName === 'string' ? payload.modelName : '',
|
|
1524
|
+
systemPrompt: typeof payload?.systemPrompt === 'string' ? payload.systemPrompt : '',
|
|
1525
|
+
disableTools: payload?.disableTools === true,
|
|
1526
|
+
};
|
|
1527
|
+
result = await callSandboxChat(chatPayload, run.controller.signal);
|
|
1528
|
+
} catch (err) {
|
|
1529
|
+
activeRuns.delete(sessionId);
|
|
1530
|
+
if (run.aborted) {
|
|
1531
|
+
emit({ type: 'assistant_abort', sessionId, ts: new Date().toISOString() });
|
|
1532
|
+
return { ok: true, aborted: true };
|
|
1533
|
+
}
|
|
1534
|
+
const errText = '[sandbox error] ' + (err?.message || String(err));
|
|
1535
|
+
assistantMsg.text = errText;
|
|
1536
|
+
emit({ type: 'assistant_delta', sessionId, delta: errText });
|
|
1537
|
+
emit({ type: 'assistant_end', sessionId, message: clone(assistantMsg), error: errText });
|
|
1538
|
+
return { ok: false, error: errText };
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
if (run.aborted) {
|
|
1542
|
+
activeRuns.delete(sessionId);
|
|
1543
|
+
emit({ type: 'assistant_abort', sessionId, ts: new Date().toISOString() });
|
|
1544
|
+
return { ok: true, aborted: true };
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
const toolTrace = Array.isArray(result?.toolTrace) ? result.toolTrace : [];
|
|
1548
|
+
for (const trace of toolTrace) {
|
|
1549
|
+
if (!trace) continue;
|
|
1550
|
+
if (trace.tool) {
|
|
1551
|
+
emit({ type: 'tool_call', sessionId, tool: trace.tool, args: trace.args || null });
|
|
1552
|
+
}
|
|
1553
|
+
if (trace.result !== undefined) {
|
|
1554
|
+
emit({ type: 'tool_result', sessionId, tool: trace.tool, result: trace.result });
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
const out = typeof result?.content === 'string' ? result.content : '';
|
|
1559
|
+
if (!out) {
|
|
1560
|
+
activeRuns.delete(sessionId);
|
|
1561
|
+
emit({ type: 'assistant_end', sessionId, message: clone(assistantMsg) });
|
|
1562
|
+
return { ok: true };
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
const chunks = [];
|
|
1566
|
+
for (let i = 0; i < out.length; i += 16) chunks.push(out.slice(i, i + 16));
|
|
1567
|
+
|
|
581
1568
|
chunks.forEach((delta, idx) => {
|
|
582
1569
|
const t = setTimeout(() => {
|
|
583
1570
|
if (run.aborted) return;
|
|
@@ -587,113 +1574,297 @@ const host = {
|
|
|
587
1574
|
activeRuns.delete(sessionId);
|
|
588
1575
|
emit({ type: 'assistant_end', sessionId, message: clone(assistantMsg) });
|
|
589
1576
|
}
|
|
590
|
-
},
|
|
1577
|
+
}, 50 + idx * 40);
|
|
591
1578
|
run.timers.push(t);
|
|
592
1579
|
});
|
|
593
1580
|
|
|
594
1581
|
return { ok: true };
|
|
595
1582
|
};
|
|
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
|
+
|
|
1677
|
+
const entryUrl = `/plugin/${encodeURIComponent(entryRel).replaceAll('%2F', '/')}`;
|
|
596
1678
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
if (typeof fn !== 'function') throw new Error('listener is required');
|
|
600
|
-
const sub = { filter: filter && typeof filter === 'object' ? { ...filter } : {}, fn };
|
|
601
|
-
listeners.add(sub);
|
|
602
|
-
return () => listeners.delete(sub);
|
|
603
|
-
},
|
|
604
|
-
unsubscribe: () => (listeners.clear(), { ok: true }),
|
|
605
|
-
};
|
|
606
|
-
|
|
607
|
-
return {
|
|
608
|
-
agents: agentsApi,
|
|
609
|
-
sessions: sessionsApi,
|
|
610
|
-
messages: messagesApi,
|
|
611
|
-
send,
|
|
612
|
-
abort,
|
|
613
|
-
events,
|
|
614
|
-
};
|
|
615
|
-
})(),
|
|
616
|
-
};
|
|
617
|
-
|
|
618
|
-
let dispose = null;
|
|
619
|
-
|
|
620
|
-
async function loadAndMount() {
|
|
621
|
-
if (typeof dispose === 'function') { try { await dispose(); } catch {} dispose = null; }
|
|
622
|
-
container.textContent = '';
|
|
623
|
-
|
|
624
|
-
const entryUrl = __SANDBOX__.entryUrl;
|
|
625
|
-
const mod = await import(entryUrl + (entryUrl.includes('?') ? '&' : '?') + 't=' + Date.now());
|
|
626
|
-
const mount = mod?.mount || mod?.default?.mount || (typeof mod?.default === 'function' ? mod.default : null);
|
|
627
|
-
if (typeof mount !== 'function') throw new Error('module entry must export mount()');
|
|
628
|
-
const ret = await mount({ container, host, slots: { header: headerSlot } });
|
|
629
|
-
if (typeof ret === 'function') dispose = ret;
|
|
630
|
-
else if (ret && typeof ret.dispose === 'function') dispose = () => ret.dispose();
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
const renderError = (e) => {
|
|
634
|
-
const pre = document.createElement('pre');
|
|
635
|
-
pre.style.padding = '12px';
|
|
636
|
-
pre.style.whiteSpace = 'pre-wrap';
|
|
637
|
-
pre.textContent = '[sandbox] ' + (e?.stack || e?.message || String(e));
|
|
638
|
-
container.appendChild(pre);
|
|
639
|
-
};
|
|
1679
|
+
let backendInstance = null;
|
|
1680
|
+
let backendFactory = null;
|
|
640
1681
|
|
|
641
|
-
const
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
1682
|
+
const { primary: sandboxRoot, legacy: legacySandboxRoot } = resolveSandboxRoots();
|
|
1683
|
+
const sandboxConfigPath = resolveSandboxConfigPath({ primaryRoot: sandboxRoot, legacyRoot: legacySandboxRoot });
|
|
1684
|
+
let sandboxLlmConfig = loadSandboxLlmConfig(sandboxConfigPath);
|
|
1685
|
+
const getAppMcpPrompt = () => resolveAppMcpPrompt(app, pluginDir);
|
|
1686
|
+
const appMcpEntry = buildAppMcpEntry({ pluginDir, pluginId: String(manifest?.id || ''), app });
|
|
1687
|
+
|
|
1688
|
+
let mcpRuntime = null;
|
|
1689
|
+
let mcpRuntimePromise = null;
|
|
1690
|
+
let sandboxCallMeta = null;
|
|
1691
|
+
|
|
1692
|
+
const resetMcpRuntime = async () => {
|
|
1693
|
+
const runtime = mcpRuntime;
|
|
1694
|
+
mcpRuntime = null;
|
|
1695
|
+
mcpRuntimePromise = null;
|
|
1696
|
+
if (runtime?.transport && typeof runtime.transport.close === 'function') {
|
|
1697
|
+
try {
|
|
1698
|
+
await runtime.transport.close();
|
|
1699
|
+
} catch {
|
|
1700
|
+
// ignore
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
if (runtime?.client && typeof runtime.client.close === 'function') {
|
|
1704
|
+
try {
|
|
1705
|
+
await runtime.client.close();
|
|
1706
|
+
} catch {
|
|
1707
|
+
// ignore
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
649
1710
|
};
|
|
650
|
-
})();
|
|
651
1711
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
1712
|
+
const ensureMcpRuntime = async () => {
|
|
1713
|
+
if (!appMcpEntry) return null;
|
|
1714
|
+
if (mcpRuntime) return mcpRuntime;
|
|
1715
|
+
if (!mcpRuntimePromise) {
|
|
1716
|
+
mcpRuntimePromise = (async () => {
|
|
1717
|
+
const handle = await connectMcpServer(appMcpEntry);
|
|
1718
|
+
if (!handle) return null;
|
|
1719
|
+
const toolEntries = Array.isArray(handle.tools)
|
|
1720
|
+
? handle.tools.map((tool) => {
|
|
1721
|
+
const identifier = buildMcpToolIdentifier(handle.serverName, tool?.name);
|
|
1722
|
+
return {
|
|
1723
|
+
identifier,
|
|
1724
|
+
serverName: handle.serverName,
|
|
1725
|
+
toolName: tool?.name,
|
|
1726
|
+
client: handle.client,
|
|
1727
|
+
definition: {
|
|
1728
|
+
type: 'function',
|
|
1729
|
+
function: {
|
|
1730
|
+
name: identifier,
|
|
1731
|
+
description: buildMcpToolDescription(handle.serverName, tool),
|
|
1732
|
+
parameters:
|
|
1733
|
+
tool?.inputSchema && typeof tool.inputSchema === 'object'
|
|
1734
|
+
? tool.inputSchema
|
|
1735
|
+
: { type: 'object', properties: {} },
|
|
1736
|
+
},
|
|
1737
|
+
},
|
|
1738
|
+
};
|
|
1739
|
+
})
|
|
1740
|
+
: [];
|
|
1741
|
+
const toolMap = new Map(toolEntries.map((entry) => [entry.identifier, entry]));
|
|
1742
|
+
return { ...handle, toolEntries, toolMap };
|
|
1743
|
+
})();
|
|
1744
|
+
}
|
|
1745
|
+
mcpRuntime = await mcpRuntimePromise;
|
|
1746
|
+
return mcpRuntime;
|
|
1747
|
+
};
|
|
660
1748
|
|
|
661
|
-
|
|
662
|
-
`;
|
|
663
|
-
}
|
|
1749
|
+
const getSandboxLlmConfig = () => ({ ...sandboxLlmConfig });
|
|
664
1750
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
1751
|
+
const updateSandboxLlmConfig = (patch) => {
|
|
1752
|
+
if (!patch || typeof patch !== 'object') return getSandboxLlmConfig();
|
|
1753
|
+
const next = { ...sandboxLlmConfig };
|
|
1754
|
+
if (Object.prototype.hasOwnProperty.call(patch, 'apiKey')) {
|
|
1755
|
+
next.apiKey = normalizeText(patch.apiKey);
|
|
1756
|
+
}
|
|
1757
|
+
if (Object.prototype.hasOwnProperty.call(patch, 'baseUrl')) {
|
|
1758
|
+
next.baseUrl = normalizeText(patch.baseUrl);
|
|
1759
|
+
}
|
|
1760
|
+
if (Object.prototype.hasOwnProperty.call(patch, 'modelId')) {
|
|
1761
|
+
next.modelId = normalizeText(patch.modelId);
|
|
1762
|
+
}
|
|
1763
|
+
if (Object.prototype.hasOwnProperty.call(patch, 'workdir')) {
|
|
1764
|
+
next.workdir = normalizeText(patch.workdir);
|
|
1765
|
+
}
|
|
1766
|
+
sandboxLlmConfig = next;
|
|
1767
|
+
saveSandboxLlmConfig(sandboxConfigPath, next);
|
|
1768
|
+
return { ...next };
|
|
1769
|
+
};
|
|
683
1770
|
|
|
684
|
-
const
|
|
685
|
-
|
|
1771
|
+
const runSandboxChat = async ({ messages, modelId, modelName, systemPrompt, disableTools, signal } = {}) => {
|
|
1772
|
+
const cfg = getSandboxLlmConfig();
|
|
1773
|
+
const apiKey = normalizeText(cfg.apiKey || process.env.SANDBOX_LLM_API_KEY);
|
|
1774
|
+
const baseUrl = normalizeText(cfg.baseUrl) || DEFAULT_LLM_BASE_URL;
|
|
1775
|
+
const effectiveModel = normalizeText(modelId) || normalizeText(modelName) || normalizeText(cfg.modelId);
|
|
1776
|
+
if (!apiKey) {
|
|
1777
|
+
throw new Error('Sandbox API key not configured. Use "AI Config" in the sandbox toolbar.');
|
|
1778
|
+
}
|
|
1779
|
+
if (!effectiveModel) {
|
|
1780
|
+
throw new Error('Sandbox modelId not configured. Use "AI Config" in the sandbox toolbar.');
|
|
1781
|
+
}
|
|
686
1782
|
|
|
687
|
-
|
|
1783
|
+
const prompt = normalizeText(systemPrompt) || (!disableTools ? normalizeText(getAppMcpPrompt()) : '');
|
|
1784
|
+
const openAiMessages = [];
|
|
1785
|
+
if (prompt) openAiMessages.push({ role: 'system', content: prompt });
|
|
1786
|
+
const inputMessages = Array.isArray(messages) ? messages : [];
|
|
1787
|
+
for (const msg of inputMessages) {
|
|
1788
|
+
const role = normalizeText(msg?.role);
|
|
1789
|
+
if (role !== 'user' && role !== 'assistant' && role !== 'system') continue;
|
|
1790
|
+
const text = typeof msg?.text === 'string' ? msg.text : typeof msg?.content === 'string' ? msg.content : '';
|
|
1791
|
+
if (!text || !text.trim()) continue;
|
|
1792
|
+
openAiMessages.push({ role, content: String(text) });
|
|
1793
|
+
}
|
|
1794
|
+
if (openAiMessages.length === 0) throw new Error('No input messages provided.');
|
|
1795
|
+
|
|
1796
|
+
let toolEntries = [];
|
|
1797
|
+
let toolMap = new Map();
|
|
1798
|
+
if (!disableTools) {
|
|
1799
|
+
const runtime = await ensureMcpRuntime();
|
|
1800
|
+
if (runtime?.toolEntries?.length) {
|
|
1801
|
+
toolEntries = runtime.toolEntries;
|
|
1802
|
+
toolMap = runtime.toolMap || new Map();
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
const toolDefs = toolEntries.map((entry) => entry.definition);
|
|
1806
|
+
|
|
1807
|
+
const toolTrace = [];
|
|
1808
|
+
let iteration = 0;
|
|
1809
|
+
const maxToolPasses = 8;
|
|
1810
|
+
let workingMessages = openAiMessages.slice();
|
|
1811
|
+
|
|
1812
|
+
while (iteration < maxToolPasses) {
|
|
1813
|
+
const response = await callOpenAiChat({
|
|
1814
|
+
apiKey,
|
|
1815
|
+
baseUrl,
|
|
1816
|
+
model: effectiveModel,
|
|
1817
|
+
messages: workingMessages,
|
|
1818
|
+
tools: toolDefs,
|
|
1819
|
+
signal,
|
|
1820
|
+
});
|
|
1821
|
+
const message = response?.choices?.[0]?.message;
|
|
1822
|
+
if (!message) throw new Error('Empty model response.');
|
|
1823
|
+
const content = typeof message.content === 'string' ? message.content : '';
|
|
1824
|
+
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
1825
|
+
if (toolCalls.length > 0 && toolMap.size > 0 && !disableTools) {
|
|
1826
|
+
workingMessages.push({ role: 'assistant', content, tool_calls: toolCalls });
|
|
1827
|
+
for (const call of toolCalls) {
|
|
1828
|
+
const toolName = typeof call?.function?.name === 'string' ? call.function.name : '';
|
|
1829
|
+
const toolEntry = toolName ? toolMap.get(toolName) : null;
|
|
1830
|
+
let args = {};
|
|
1831
|
+
let resultText = '';
|
|
1832
|
+
if (!toolEntry) {
|
|
1833
|
+
resultText = `[error] Tool not registered: ${toolName || 'unknown'}`;
|
|
1834
|
+
} else {
|
|
1835
|
+
const rawArgs = typeof call?.function?.arguments === 'string' ? call.function.arguments : '{}';
|
|
1836
|
+
try {
|
|
1837
|
+
args = JSON.parse(rawArgs || '{}');
|
|
1838
|
+
} catch (err) {
|
|
1839
|
+
resultText = '[error] Failed to parse tool arguments: ' + (err?.message || String(err));
|
|
1840
|
+
args = {};
|
|
1841
|
+
}
|
|
1842
|
+
if (!resultText) {
|
|
1843
|
+
const toolResult = await toolEntry.client.callTool({
|
|
1844
|
+
name: toolEntry.toolName,
|
|
1845
|
+
arguments: args,
|
|
1846
|
+
...(sandboxCallMeta ? { _meta: sandboxCallMeta } : {}),
|
|
1847
|
+
});
|
|
1848
|
+
resultText = formatMcpToolResult(toolEntry.serverName, toolEntry.toolName, toolResult);
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
toolTrace.push({ tool: toolName || 'unknown', args, result: resultText });
|
|
1852
|
+
workingMessages.push({ role: 'tool', tool_call_id: String(call?.id || ''), content: resultText });
|
|
1853
|
+
}
|
|
1854
|
+
iteration += 1;
|
|
1855
|
+
continue;
|
|
1856
|
+
}
|
|
1857
|
+
return { content, model: effectiveModel, toolTrace };
|
|
1858
|
+
}
|
|
688
1859
|
|
|
689
|
-
|
|
690
|
-
|
|
1860
|
+
throw new Error('Too many tool calls. Aborting.');
|
|
1861
|
+
};
|
|
691
1862
|
|
|
692
1863
|
const ctxBase = {
|
|
693
1864
|
pluginId: String(manifest?.id || ''),
|
|
694
1865
|
pluginDir,
|
|
695
|
-
stateDir: path.join(
|
|
696
|
-
sessionRoot: process.cwd(),
|
|
1866
|
+
stateDir: path.join(sandboxRoot, 'state', 'chatos'),
|
|
1867
|
+
sessionRoot: process.cwd(),
|
|
697
1868
|
projectRoot: process.cwd(),
|
|
698
1869
|
dataDir: '',
|
|
699
1870
|
llm: {
|
|
@@ -701,108 +1872,199 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
|
|
|
701
1872
|
const input = typeof payload?.input === 'string' ? payload.input : '';
|
|
702
1873
|
const normalized = String(input || '').trim();
|
|
703
1874
|
if (!normalized) throw new Error('input is required');
|
|
704
|
-
const
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
1875
|
+
const result = await runSandboxChat({
|
|
1876
|
+
messages: [{ role: 'user', text: normalized }],
|
|
1877
|
+
modelId: typeof payload?.modelId === 'string' ? payload.modelId : '',
|
|
1878
|
+
modelName: typeof payload?.modelName === 'string' ? payload.modelName : '',
|
|
1879
|
+
systemPrompt: typeof payload?.systemPrompt === 'string' ? payload.systemPrompt : '',
|
|
1880
|
+
disableTools: payload?.disableTools === true,
|
|
1881
|
+
});
|
|
710
1882
|
return {
|
|
711
1883
|
ok: true,
|
|
712
|
-
model:
|
|
713
|
-
content:
|
|
1884
|
+
model: result.model,
|
|
1885
|
+
content: result.content,
|
|
1886
|
+
toolTrace: result.toolTrace,
|
|
714
1887
|
};
|
|
715
1888
|
},
|
|
716
1889
|
},
|
|
717
1890
|
};
|
|
718
|
-
ctxBase.dataDir = path.join(
|
|
1891
|
+
ctxBase.dataDir = path.join(sandboxRoot, 'data', ctxBase.pluginId);
|
|
719
1892
|
ensureDir(ctxBase.stateDir);
|
|
720
1893
|
ensureDir(ctxBase.dataDir);
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
1894
|
+
sandboxCallMeta = buildSandboxCallMeta({
|
|
1895
|
+
rawCallMeta: app?.ai?.mcp?.callMeta,
|
|
1896
|
+
rawWorkdir: getSandboxLlmConfig().workdir,
|
|
1897
|
+
context: {
|
|
1898
|
+
pluginId: ctxBase.pluginId,
|
|
1899
|
+
appId: effectiveAppId,
|
|
1900
|
+
pluginDir: ctxBase.pluginDir,
|
|
1901
|
+
dataDir: ctxBase.dataDir,
|
|
1902
|
+
stateDir: ctxBase.stateDir,
|
|
1903
|
+
sessionRoot: ctxBase.sessionRoot,
|
|
1904
|
+
projectRoot: ctxBase.projectRoot,
|
|
1905
|
+
},
|
|
1906
|
+
});
|
|
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
|
+
|
|
745
1931
|
changeSeq += 1;
|
|
746
1932
|
if (rel.startsWith('backend/')) {
|
|
747
1933
|
backendInstance = null;
|
|
748
1934
|
backendFactory = null;
|
|
749
1935
|
}
|
|
1936
|
+
resetMcpRuntime().catch(() => {});
|
|
750
1937
|
sseBroadcast('reload', { seq: changeSeq, eventType: eventType || '', path: rel });
|
|
751
1938
|
});
|
|
752
|
-
|
|
753
|
-
const server = http.createServer(async (req, res) => {
|
|
754
|
-
try {
|
|
755
|
-
const parsed = url.parse(req.url || '/', true);
|
|
756
|
-
const pathname = parsed.pathname || '/';
|
|
757
|
-
|
|
758
|
-
if (req.method === 'GET' && pathname === '/') {
|
|
759
|
-
return sendText(res, 200, htmlPage(), 'text/html; charset=utf-8');
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
if (req.method === 'GET' && pathname === '/events') {
|
|
763
|
-
res.writeHead(200, {
|
|
764
|
-
'content-type': 'text/event-stream; charset=utf-8',
|
|
765
|
-
'cache-control': 'no-store',
|
|
766
|
-
connection: 'keep-alive',
|
|
767
|
-
});
|
|
768
|
-
res.write(': connected\n\n');
|
|
769
|
-
sseClients.add(res);
|
|
770
|
-
const ping = setInterval(() => {
|
|
771
|
-
try {
|
|
772
|
-
res.write(': ping\n\n');
|
|
773
|
-
} catch {
|
|
774
|
-
// ignore
|
|
775
|
-
}
|
|
776
|
-
}, 15000);
|
|
777
|
-
req.on('close', () => {
|
|
778
|
-
try {
|
|
779
|
-
clearInterval(ping);
|
|
780
|
-
} catch {
|
|
781
|
-
// ignore
|
|
782
|
-
}
|
|
783
|
-
sseClients.delete(res);
|
|
784
|
-
});
|
|
785
|
-
return;
|
|
786
|
-
}
|
|
787
|
-
|
|
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
|
+
|
|
788
1975
|
if (req.method === 'GET' && pathname === '/sandbox.mjs') {
|
|
1976
|
+
const tokenNames = loadTokenNames();
|
|
1977
|
+
const sandboxContext = {
|
|
1978
|
+
pluginId: ctxBase.pluginId,
|
|
1979
|
+
appId: effectiveAppId,
|
|
1980
|
+
pluginDir: ctxBase.pluginDir,
|
|
1981
|
+
dataDir: ctxBase.dataDir,
|
|
1982
|
+
stateDir: ctxBase.stateDir,
|
|
1983
|
+
sessionRoot: ctxBase.sessionRoot,
|
|
1984
|
+
projectRoot: ctxBase.projectRoot,
|
|
1985
|
+
workdir: sandboxCallMeta?.workdir || ctxBase.dataDir || '',
|
|
1986
|
+
};
|
|
789
1987
|
const js = sandboxClientJs()
|
|
1988
|
+
.replaceAll('__SANDBOX__.context', JSON.stringify(sandboxContext))
|
|
790
1989
|
.replaceAll('__SANDBOX__.pluginId', JSON.stringify(ctxBase.pluginId))
|
|
791
1990
|
.replaceAll('__SANDBOX__.appId', JSON.stringify(effectiveAppId))
|
|
792
1991
|
.replaceAll('__SANDBOX__.entryUrl', JSON.stringify(entryUrl))
|
|
793
|
-
.replaceAll('__SANDBOX__.registryApp', JSON.stringify({ plugin: { id: ctxBase.pluginId }, id: effectiveAppId, entry: { type: 'module', url: entryUrl } }))
|
|
1992
|
+
.replaceAll('__SANDBOX__.registryApp', JSON.stringify({ plugin: { id: ctxBase.pluginId }, id: effectiveAppId, entry: { type: 'module', url: entryUrl } }))
|
|
1993
|
+
.replaceAll('__SANDBOX__.tokenNames', JSON.stringify(tokenNames));
|
|
794
1994
|
return sendText(res, 200, js, 'text/javascript; charset=utf-8');
|
|
795
1995
|
}
|
|
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
|
+
|
|
2004
|
+
if (req.method === 'GET' && pathname === '/api/manifest') {
|
|
2005
|
+
return sendJson(res, 200, { ok: true, manifest });
|
|
2006
|
+
}
|
|
796
2007
|
|
|
797
|
-
if (
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
2008
|
+
if (pathname === '/api/sandbox/llm-config') {
|
|
2009
|
+
if (req.method === 'GET') {
|
|
2010
|
+
const cfg = getSandboxLlmConfig();
|
|
2011
|
+
return sendJson(res, 200, {
|
|
2012
|
+
ok: true,
|
|
2013
|
+
config: {
|
|
2014
|
+
baseUrl: cfg.baseUrl || '',
|
|
2015
|
+
modelId: cfg.modelId || '',
|
|
2016
|
+
workdir: cfg.workdir || '',
|
|
2017
|
+
hasApiKey: Boolean(cfg.apiKey),
|
|
2018
|
+
},
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
if (req.method === 'POST') {
|
|
2022
|
+
try {
|
|
2023
|
+
const payload = await readJsonBody(req);
|
|
2024
|
+
const patch = payload?.config && typeof payload.config === 'object' ? payload.config : payload;
|
|
2025
|
+
const next = updateSandboxLlmConfig({
|
|
2026
|
+
...(Object.prototype.hasOwnProperty.call(patch || {}, 'apiKey') ? { apiKey: patch.apiKey } : {}),
|
|
2027
|
+
...(Object.prototype.hasOwnProperty.call(patch || {}, 'baseUrl') ? { baseUrl: patch.baseUrl } : {}),
|
|
2028
|
+
...(Object.prototype.hasOwnProperty.call(patch || {}, 'modelId') ? { modelId: patch.modelId } : {}),
|
|
2029
|
+
...(Object.prototype.hasOwnProperty.call(patch || {}, 'workdir') ? { workdir: patch.workdir } : {}),
|
|
2030
|
+
});
|
|
2031
|
+
return sendJson(res, 200, {
|
|
2032
|
+
ok: true,
|
|
2033
|
+
config: {
|
|
2034
|
+
baseUrl: next.baseUrl || '',
|
|
2035
|
+
modelId: next.modelId || '',
|
|
2036
|
+
workdir: next.workdir || '',
|
|
2037
|
+
hasApiKey: Boolean(next.apiKey),
|
|
2038
|
+
},
|
|
2039
|
+
});
|
|
2040
|
+
} catch (err) {
|
|
2041
|
+
return sendJson(res, 200, { ok: false, message: err?.message || String(err) });
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
return sendJson(res, 405, { ok: false, message: 'Method not allowed' });
|
|
802
2045
|
}
|
|
803
2046
|
|
|
804
|
-
if (
|
|
805
|
-
return sendJson(res,
|
|
2047
|
+
if (pathname === '/api/llm/chat') {
|
|
2048
|
+
if (req.method !== 'POST') return sendJson(res, 405, { ok: false, message: 'Method not allowed' });
|
|
2049
|
+
try {
|
|
2050
|
+
const payload = await readJsonBody(req);
|
|
2051
|
+
const messages = Array.isArray(payload?.messages) ? payload.messages : [];
|
|
2052
|
+
const result = await runSandboxChat({
|
|
2053
|
+
messages,
|
|
2054
|
+
modelId: typeof payload?.modelId === 'string' ? payload.modelId : '',
|
|
2055
|
+
modelName: typeof payload?.modelName === 'string' ? payload.modelName : '',
|
|
2056
|
+
systemPrompt: typeof payload?.systemPrompt === 'string' ? payload.systemPrompt : '',
|
|
2057
|
+
disableTools: payload?.disableTools === true,
|
|
2058
|
+
});
|
|
2059
|
+
return sendJson(res, 200, {
|
|
2060
|
+
ok: true,
|
|
2061
|
+
model: result.model,
|
|
2062
|
+
content: result.content,
|
|
2063
|
+
toolTrace: result.toolTrace || [],
|
|
2064
|
+
});
|
|
2065
|
+
} catch (err) {
|
|
2066
|
+
return sendJson(res, 200, { ok: false, message: err?.message || String(err) });
|
|
2067
|
+
}
|
|
806
2068
|
}
|
|
807
2069
|
|
|
808
2070
|
if (pathname === '/api/backend/invoke') {
|
|
@@ -810,52 +2072,55 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
|
|
|
810
2072
|
let body = '';
|
|
811
2073
|
req.on('data', (chunk) => {
|
|
812
2074
|
body += chunk;
|
|
813
|
-
});
|
|
814
|
-
req.on('end', async () => {
|
|
815
|
-
try {
|
|
816
|
-
const payload = body ? JSON.parse(body) : {};
|
|
817
|
-
const method = typeof payload?.method === 'string' ? payload.method.trim() : '';
|
|
818
|
-
if (!method) return sendJson(res, 400, { ok: false, message: 'method is required' });
|
|
819
|
-
const params = payload?.params;
|
|
820
|
-
|
|
821
|
-
if (!backendFactory) backendFactory = await loadBackendFactory({ pluginDir, manifest });
|
|
822
|
-
if (!backendFactory) return sendJson(res, 200, { ok: false, message: 'backend not configured in plugin.json' });
|
|
823
|
-
|
|
824
|
-
if (!backendInstance || typeof backendInstance !== 'object' || !backendInstance.methods) {
|
|
825
|
-
backendInstance = await backendFactory({ ...ctxBase });
|
|
826
|
-
}
|
|
827
|
-
const fn = backendInstance?.methods?.[method];
|
|
828
|
-
if (typeof fn !== 'function') return sendJson(res, 404, { ok: false, message: `method not found: ${method}` });
|
|
829
|
-
const result = await fn(params, { ...ctxBase });
|
|
830
|
-
return sendJson(res, 200, { ok: true, result });
|
|
831
|
-
} catch (e) {
|
|
832
|
-
return sendJson(res, 200, { ok: false, message: e?.message || String(e) });
|
|
833
|
-
}
|
|
834
|
-
});
|
|
835
|
-
return;
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
sendText(res, 404, 'Not found');
|
|
839
|
-
} catch (e) {
|
|
840
|
-
sendJson(res, 500, { ok: false, message: e?.message || String(e) });
|
|
841
|
-
}
|
|
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
|
+
});
|
|
2105
|
+
server.once('close', () => {
|
|
2106
|
+
stopWatch();
|
|
2107
|
+
resetMcpRuntime().catch(() => {});
|
|
842
2108
|
});
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
server.
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
pluginDir
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
}
|
|
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
|
+
}
|