@leeoohoo/ui-apps-devkit 0.1.1 → 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 +25 -16
- package/package.json +6 -3
- package/src/commands/init.js +14 -13
- package/src/commands/install.js +12 -11
- package/src/commands/validate.js +69 -21
- package/src/lib/state-constants.js +2 -0
- package/src/sandbox/server.js +1145 -219
- package/templates/basic/README.md +19 -14
- package/templates/basic/docs/CHATOS_UI_APPS_AI_CONTRIBUTIONS.md +36 -34
- package/templates/basic/docs/CHATOS_UI_APPS_BACKEND_PROTOCOL.md +3 -2
- package/templates/basic/docs/CHATOS_UI_APPS_HOST_API.md +1 -1
- package/templates/basic/docs/CHATOS_UI_APPS_OVERVIEW.md +20 -18
- package/templates/basic/docs/CHATOS_UI_APPS_PLUGIN_MANIFEST.md +23 -22
- package/templates/basic/docs/CHATOS_UI_APPS_TROUBLESHOOTING.md +5 -5
- package/templates/basic/docs/CHATOS_UI_PROMPTS_PROTOCOL.md +3 -3
- package/templates/notepad/README.md +28 -19
- package/templates/notepad/docs/CHATOS_UI_APPS_AI_CONTRIBUTIONS.md +36 -34
- package/templates/notepad/docs/CHATOS_UI_APPS_BACKEND_PROTOCOL.md +3 -2
- package/templates/notepad/docs/CHATOS_UI_APPS_HOST_API.md +1 -1
- package/templates/notepad/docs/CHATOS_UI_APPS_OVERVIEW.md +20 -18
- package/templates/notepad/docs/CHATOS_UI_APPS_PLUGIN_MANIFEST.md +23 -22
- package/templates/notepad/docs/CHATOS_UI_APPS_TROUBLESHOOTING.md +5 -5
- package/templates/notepad/docs/CHATOS_UI_PROMPTS_PROTOCOL.md +3 -3
- package/templates/notepad/plugin/apps/app/mcp-server.mjs +8 -1
- package/templates/notepad/plugin/shared/notepad-paths.mjs +41 -23
package/src/sandbox/server.js
CHANGED
|
@@ -1,37 +1,447 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
-
import http from 'http';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import url from 'url';
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
2
|
+
import http from 'http';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import url from 'url';
|
|
5
|
+
|
|
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';
|
|
13
|
+
import { loadPluginManifest, pickAppFromManifest } from '../lib/plugin.js';
|
|
14
|
+
import { resolveInsideDir } from '../lib/path-boundary.js';
|
|
15
|
+
import { COMPAT_STATE_ROOT_DIRNAME, STATE_ROOT_DIRNAME } from '../lib/state-constants.js';
|
|
9
16
|
|
|
10
17
|
const __filename = url.fileURLToPath(import.meta.url);
|
|
11
|
-
const __dirname = path.dirname(__filename);
|
|
12
|
-
|
|
13
|
-
const TOKEN_REGEX = /--ds-[a-z0-9-]+/gi;
|
|
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;
|
|
14
23
|
const GLOBAL_STYLES_CANDIDATES = [
|
|
15
24
|
path.resolve(__dirname, '..', '..', '..', 'common', 'aide-ui', 'components', 'GlobalStyles.jsx'),
|
|
16
25
|
path.resolve(process.cwd(), 'common', 'aide-ui', 'components', 'GlobalStyles.jsx'),
|
|
17
26
|
];
|
|
18
27
|
|
|
19
|
-
function loadTokenNames() {
|
|
20
|
-
for (const candidate of GLOBAL_STYLES_CANDIDATES) {
|
|
21
|
-
try {
|
|
22
|
-
if (!isFile(candidate)) continue;
|
|
23
|
-
const raw = fs.readFileSync(candidate, 'utf8');
|
|
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');
|
|
24
33
|
const matches = raw.match(TOKEN_REGEX) || [];
|
|
25
34
|
const names = Array.from(new Set(matches.map((v) => v.toLowerCase())));
|
|
26
35
|
if (names.length > 0) return names.sort();
|
|
27
36
|
} catch {
|
|
28
37
|
// ignore
|
|
29
38
|
}
|
|
30
|
-
}
|
|
31
|
-
return [];
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
39
|
+
}
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
|
|
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 };
|
|
55
|
+
}
|
|
56
|
+
|
|
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;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const DEFAULT_LLM_BASE_URL = 'https://api.openai.com/v1';
|
|
66
|
+
|
|
67
|
+
function normalizeText(value) {
|
|
68
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isPlainObject(value) {
|
|
72
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
73
|
+
}
|
|
74
|
+
|
|
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
|
+
}
|
|
88
|
+
|
|
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
|
+
}
|
|
106
|
+
|
|
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
|
+
}
|
|
128
|
+
|
|
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 || '',
|
|
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 });
|
|
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
|
+
}
|
|
199
|
+
|
|
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 = [];
|
|
337
|
+
try {
|
|
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}`);
|
|
345
|
+
}
|
|
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}`);
|
|
354
|
+
}
|
|
355
|
+
throw new Error(`Failed to connect MCP server (${serverName}): ${errors.join(' | ')}`);
|
|
356
|
+
}
|
|
357
|
+
|
|
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,
|
|
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
|
+
}
|
|
444
|
+
|
|
35
445
|
function sendJson(res, status, obj) {
|
|
36
446
|
const raw = JSON.stringify(obj);
|
|
37
447
|
res.writeHead(status, {
|
|
@@ -41,16 +451,33 @@ function sendJson(res, status, obj) {
|
|
|
41
451
|
res.end(raw);
|
|
42
452
|
}
|
|
43
453
|
|
|
44
|
-
function sendText(res, status, text, contentType) {
|
|
45
|
-
res.writeHead(status, {
|
|
46
|
-
'content-type': contentType || 'text/plain; charset=utf-8',
|
|
47
|
-
'cache-control': 'no-store',
|
|
48
|
-
});
|
|
49
|
-
res.end(text);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function
|
|
53
|
-
|
|
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
|
+
}
|
|
461
|
+
|
|
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({});
|
|
470
|
+
try {
|
|
471
|
+
resolve(JSON.parse(body));
|
|
472
|
+
} catch (err) {
|
|
473
|
+
reject(err);
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function guessContentType(filePath) {
|
|
480
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
54
481
|
if (ext === '.html') return 'text/html; charset=utf-8';
|
|
55
482
|
if (ext === '.css') return 'text/css; charset=utf-8';
|
|
56
483
|
if (ext === '.mjs' || ext === '.js') return 'text/javascript; charset=utf-8';
|
|
@@ -271,17 +698,46 @@ function htmlPage() {
|
|
|
271
698
|
justify-content: space-between;
|
|
272
699
|
border-bottom: 1px solid var(--ds-panel-border);
|
|
273
700
|
}
|
|
274
|
-
#sandboxInspectorBody {
|
|
275
|
-
padding: 10px 12px;
|
|
276
|
-
overflow: auto;
|
|
277
|
-
display: flex;
|
|
278
|
-
flex-direction: column;
|
|
279
|
-
gap: 10px;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
701
|
+
#sandboxInspectorBody {
|
|
702
|
+
padding: 10px 12px;
|
|
703
|
+
overflow: auto;
|
|
704
|
+
display: flex;
|
|
705
|
+
flex-direction: column;
|
|
706
|
+
gap: 10px;
|
|
707
|
+
}
|
|
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%;
|
|
285
741
|
padding:8px;
|
|
286
742
|
border-radius:10px;
|
|
287
743
|
border:1px solid var(--ds-panel-border);
|
|
@@ -308,32 +764,68 @@ function htmlPage() {
|
|
|
308
764
|
<button id="btnThemeDark" class="btn" type="button">Dark</button>
|
|
309
765
|
<button id="btnThemeSystem" class="btn" type="button">System</button>
|
|
310
766
|
</div>
|
|
311
|
-
<div id="themeStatus" class="muted"></div>
|
|
312
|
-
<div id="sandboxContext" class="muted"></div>
|
|
313
|
-
<button id="
|
|
314
|
-
<button id="
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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>
|
|
771
|
+
<button id="btnReload" class="btn" type="button">Reload</button>
|
|
772
|
+
</div>
|
|
773
|
+
</div>
|
|
774
|
+
</div>
|
|
318
775
|
<div id="headerSlot"></div>
|
|
319
776
|
<div id="container"><div id="containerInner"></div></div>
|
|
320
777
|
</div>
|
|
321
778
|
|
|
322
779
|
<button id="promptsFab" class="btn" type="button">:)</button>
|
|
323
780
|
|
|
324
|
-
<div id="promptsPanel">
|
|
325
|
-
<div id="promptsPanelHeader">
|
|
326
|
-
<div style="font-weight:800">UI Prompts</div>
|
|
327
|
-
<button id="promptsClose" class="btn" type="button">Close</button>
|
|
328
|
-
</div>
|
|
329
|
-
<div id="promptsPanelBody"></div>
|
|
330
|
-
</div>
|
|
331
|
-
|
|
332
|
-
<div id="
|
|
333
|
-
<div id="
|
|
334
|
-
<div style="font-weight:800">Sandbox
|
|
335
|
-
<div class="row">
|
|
336
|
-
<button id="
|
|
781
|
+
<div id="promptsPanel">
|
|
782
|
+
<div id="promptsPanelHeader">
|
|
783
|
+
<div style="font-weight:800">UI Prompts</div>
|
|
784
|
+
<button id="promptsClose" class="btn" type="button">Close</button>
|
|
785
|
+
</div>
|
|
786
|
+
<div id="promptsPanelBody"></div>
|
|
787
|
+
</div>
|
|
788
|
+
|
|
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>
|
|
823
|
+
|
|
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>
|
|
337
829
|
<button id="btnInspectorClose" class="btn" type="button">Close</button>
|
|
338
830
|
</div>
|
|
339
831
|
</div>
|
|
@@ -374,16 +866,28 @@ const themeStatus = $('#themeStatus');
|
|
|
374
866
|
const sandboxContext = $('#sandboxContext');
|
|
375
867
|
const btnInspectorToggle = $('#btnInspectorToggle');
|
|
376
868
|
const sandboxInspector = $('#sandboxInspector');
|
|
377
|
-
const btnInspectorClose = $('#btnInspectorClose');
|
|
378
|
-
const btnInspectorRefresh = $('#btnInspectorRefresh');
|
|
379
|
-
const inspectorContext = $('#inspectorContext');
|
|
380
|
-
const inspectorTheme = $('#inspectorTheme');
|
|
381
|
-
const inspectorTokens = $('#inspectorTokens');
|
|
382
|
-
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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');
|
|
886
|
+
|
|
887
|
+
const setPanelOpen = (open) => { panel.style.display = open ? 'flex' : 'none'; };
|
|
888
|
+
fab.addEventListener('click', () => setPanelOpen(panel.style.display !== 'flex'));
|
|
889
|
+
panelClose.addEventListener('click', () => setPanelOpen(false));
|
|
890
|
+
window.addEventListener('chatos:uiPrompts:open', () => setPanelOpen(true));
|
|
387
891
|
window.addEventListener('chatos:uiPrompts:close', () => setPanelOpen(false));
|
|
388
892
|
window.addEventListener('chatos:uiPrompts:toggle', () => setPanelOpen(panel.style.display !== 'flex'));
|
|
389
893
|
|
|
@@ -432,22 +936,83 @@ const updateThemeControls = () => {
|
|
|
432
936
|
}
|
|
433
937
|
};
|
|
434
938
|
|
|
435
|
-
const updateContextStatus = () => {
|
|
436
|
-
if (!sandboxContext) return;
|
|
437
|
-
sandboxContext.textContent = __SANDBOX__.pluginId + ':' + __SANDBOX__.appId;
|
|
438
|
-
};
|
|
439
|
-
|
|
440
|
-
const isInspectorOpen = () => sandboxInspector && sandboxInspector.style.display === 'flex';
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
939
|
+
const updateContextStatus = () => {
|
|
940
|
+
if (!sandboxContext) return;
|
|
941
|
+
sandboxContext.textContent = __SANDBOX__.pluginId + ':' + __SANDBOX__.appId;
|
|
942
|
+
};
|
|
943
|
+
|
|
944
|
+
const isInspectorOpen = () => sandboxInspector && sandboxInspector.style.display === 'flex';
|
|
945
|
+
const isLlmPanelOpen = () => llmPanel && llmPanel.style.display === 'flex';
|
|
946
|
+
|
|
947
|
+
const setLlmStatus = (text, isError) => {
|
|
948
|
+
if (!llmStatus) return;
|
|
949
|
+
llmStatus.textContent = text || '';
|
|
950
|
+
llmStatus.style.color = isError ? '#ef4444' : '';
|
|
951
|
+
};
|
|
952
|
+
|
|
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
|
+
};
|
|
969
|
+
|
|
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 : '',
|
|
977
|
+
};
|
|
978
|
+
const apiKey = llmApiKey ? llmApiKey.value : '';
|
|
979
|
+
if (clearKey) {
|
|
980
|
+
payload.apiKey = '';
|
|
981
|
+
} else if (apiKey && apiKey.trim()) {
|
|
982
|
+
payload.apiKey = apiKey.trim();
|
|
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
|
+
};
|
|
1005
|
+
|
|
1006
|
+
const formatJson = (value) => {
|
|
1007
|
+
try {
|
|
1008
|
+
return JSON.stringify(value, null, 2);
|
|
1009
|
+
} catch {
|
|
446
1010
|
return String(value);
|
|
447
1011
|
}
|
|
448
1012
|
};
|
|
449
1013
|
|
|
450
|
-
const tokenNameList = Array.isArray(__SANDBOX__.tokenNames) ? __SANDBOX__.tokenNames : [];
|
|
1014
|
+
const tokenNameList = Array.isArray(__SANDBOX__.tokenNames) ? __SANDBOX__.tokenNames : [];
|
|
1015
|
+
const sandboxContextBase = __SANDBOX__.context || { pluginId: __SANDBOX__.pluginId, appId: __SANDBOX__.appId };
|
|
451
1016
|
|
|
452
1017
|
const collectTokens = () => {
|
|
453
1018
|
const style = getComputedStyle(document.documentElement);
|
|
@@ -465,11 +1030,11 @@ const collectTokens = () => {
|
|
|
465
1030
|
.join('\\n');
|
|
466
1031
|
};
|
|
467
1032
|
|
|
468
|
-
const readHostContext = () => {
|
|
469
|
-
if (!inspectorEnabled) return null;
|
|
470
|
-
if (typeof host?.context?.get === 'function') return host.context.get();
|
|
471
|
-
return {
|
|
472
|
-
};
|
|
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
|
+
};
|
|
473
1038
|
|
|
474
1039
|
const readThemeInfo = () => ({
|
|
475
1040
|
themeMode,
|
|
@@ -539,12 +1104,17 @@ if (systemQuery && typeof systemQuery.addEventListener === 'function') {
|
|
|
539
1104
|
});
|
|
540
1105
|
}
|
|
541
1106
|
|
|
542
|
-
if (btnThemeLight) btnThemeLight.addEventListener('click', () => applyThemeMode('light'));
|
|
543
|
-
if (btnThemeDark) btnThemeDark.addEventListener('click', () => applyThemeMode('dark'));
|
|
544
|
-
if (btnThemeSystem) btnThemeSystem.addEventListener('click', () => applyThemeMode('system'));
|
|
545
|
-
if (
|
|
546
|
-
if (
|
|
547
|
-
if (
|
|
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());
|
|
548
1118
|
|
|
549
1119
|
applyThemeMode(themeMode || 'system', { persist: false });
|
|
550
1120
|
updateContextStatus();
|
|
@@ -692,15 +1262,40 @@ function renderPrompts() {
|
|
|
692
1262
|
if (source.textContent) card.appendChild(source);
|
|
693
1263
|
card.appendChild(form);
|
|
694
1264
|
panelBody.appendChild(card);
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
const
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
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
|
+
};
|
|
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
|
+
|
|
1295
|
+
const host = {
|
|
1296
|
+
bridge: { enabled: true },
|
|
1297
|
+
context: { get: () => ({ ...sandboxContextBase, theme: getTheme(), bridge: { enabled: true } }) },
|
|
1298
|
+
theme: {
|
|
704
1299
|
get: getTheme,
|
|
705
1300
|
onChange: (listener) => {
|
|
706
1301
|
if (typeof listener !== 'function') return () => {};
|
|
@@ -810,25 +1405,27 @@ const host = {
|
|
|
810
1405
|
const agentsApi = {
|
|
811
1406
|
list: async () => ({ ok: true, agents: clone(agents) }),
|
|
812
1407
|
ensureDefault: async () => ({ ok: true, agent: clone(await ensureAgent()) }),
|
|
813
|
-
create: async (payload) => {
|
|
814
|
-
const agent = {
|
|
815
|
-
id: 'sandbox-agent-' + uuid(),
|
|
816
|
-
name: payload?.name ? String(payload.name) : 'Sandbox Agent',
|
|
817
|
-
description: payload?.description ? String(payload.description) : '',
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
1408
|
+
create: async (payload) => {
|
|
1409
|
+
const agent = {
|
|
1410
|
+
id: 'sandbox-agent-' + uuid(),
|
|
1411
|
+
name: payload?.name ? String(payload.name) : 'Sandbox Agent',
|
|
1412
|
+
description: payload?.description ? String(payload.description) : '',
|
|
1413
|
+
modelId: payload?.modelId ? String(payload.modelId) : '',
|
|
1414
|
+
};
|
|
1415
|
+
agents.unshift(agent);
|
|
1416
|
+
return { ok: true, agent: clone(agent) };
|
|
1417
|
+
},
|
|
1418
|
+
update: async (id, patch) => {
|
|
823
1419
|
const agentId = String(id || '').trim();
|
|
824
1420
|
if (!agentId) throw new Error('id is required');
|
|
825
|
-
const idx = agents.findIndex((a) => a.id === agentId);
|
|
826
|
-
if (idx < 0) throw new Error('agent not found');
|
|
827
|
-
const a = agents[idx];
|
|
828
|
-
if (patch?.name) a.name = String(patch.name);
|
|
829
|
-
if (patch?.description) a.description = String(patch.description);
|
|
830
|
-
|
|
831
|
-
|
|
1421
|
+
const idx = agents.findIndex((a) => a.id === agentId);
|
|
1422
|
+
if (idx < 0) throw new Error('agent not found');
|
|
1423
|
+
const a = agents[idx];
|
|
1424
|
+
if (patch?.name) a.name = String(patch.name);
|
|
1425
|
+
if (patch?.description) a.description = String(patch.description);
|
|
1426
|
+
if (patch?.modelId) a.modelId = String(patch.modelId);
|
|
1427
|
+
return { ok: true, agent: clone(a) };
|
|
1428
|
+
},
|
|
832
1429
|
delete: async (id) => {
|
|
833
1430
|
const agentId = String(id || '').trim();
|
|
834
1431
|
if (!agentId) throw new Error('id is required');
|
|
@@ -871,14 +1468,21 @@ const host = {
|
|
|
871
1468
|
const abort = async (payload) => {
|
|
872
1469
|
const sessionId = String(payload?.sessionId || '').trim();
|
|
873
1470
|
if (!sessionId) throw new Error('sessionId is required');
|
|
874
|
-
const run = activeRuns.get(sessionId);
|
|
875
|
-
if (run) {
|
|
876
|
-
run.aborted = true;
|
|
877
|
-
|
|
878
|
-
try {
|
|
879
|
-
|
|
880
|
-
} catch {
|
|
881
|
-
// ignore
|
|
1471
|
+
const run = activeRuns.get(sessionId);
|
|
1472
|
+
if (run) {
|
|
1473
|
+
run.aborted = true;
|
|
1474
|
+
if (run.controller) {
|
|
1475
|
+
try {
|
|
1476
|
+
run.controller.abort();
|
|
1477
|
+
} catch {
|
|
1478
|
+
// ignore
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
for (const t of run.timers) {
|
|
1482
|
+
try {
|
|
1483
|
+
clearTimeout(t);
|
|
1484
|
+
} catch {
|
|
1485
|
+
// ignore
|
|
882
1486
|
}
|
|
883
1487
|
}
|
|
884
1488
|
activeRuns.delete(sessionId);
|
|
@@ -887,11 +1491,11 @@ const host = {
|
|
|
887
1491
|
return { ok: true };
|
|
888
1492
|
};
|
|
889
1493
|
|
|
890
|
-
const send = async (payload) => {
|
|
891
|
-
const sessionId = String(payload?.sessionId || '').trim();
|
|
892
|
-
const text = String(payload?.text || '').trim();
|
|
893
|
-
if (!sessionId) throw new Error('sessionId is required');
|
|
894
|
-
if (!text) throw new Error('text is required');
|
|
1494
|
+
const send = async (payload) => {
|
|
1495
|
+
const sessionId = String(payload?.sessionId || '').trim();
|
|
1496
|
+
const text = String(payload?.text || '').trim();
|
|
1497
|
+
if (!sessionId) throw new Error('sessionId is required');
|
|
1498
|
+
if (!text) throw new Error('text is required');
|
|
895
1499
|
|
|
896
1500
|
if (!sessions.has(sessionId)) throw new Error('session not found');
|
|
897
1501
|
|
|
@@ -901,32 +1505,81 @@ const host = {
|
|
|
901
1505
|
messagesBySession.set(sessionId, msgs);
|
|
902
1506
|
emit({ type: 'user_message', sessionId, message: clone(userMsg) });
|
|
903
1507
|
|
|
904
|
-
const assistantMsg = { id: 'msg-' + uuid(), role: 'assistant', text: '', ts: new Date().toISOString() };
|
|
905
|
-
msgs.push(assistantMsg);
|
|
906
|
-
emit({ type: 'assistant_start', sessionId, message: clone(assistantMsg) });
|
|
907
|
-
|
|
908
|
-
const
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
const
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
1508
|
+
const assistantMsg = { id: 'msg-' + uuid(), role: 'assistant', text: '', ts: new Date().toISOString() };
|
|
1509
|
+
msgs.push(assistantMsg);
|
|
1510
|
+
emit({ type: 'assistant_start', sessionId, message: clone(assistantMsg) });
|
|
1511
|
+
|
|
1512
|
+
const run = { aborted: false, timers: [], controller: new AbortController() };
|
|
1513
|
+
activeRuns.set(sessionId, run);
|
|
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
|
+
|
|
1568
|
+
chunks.forEach((delta, idx) => {
|
|
1569
|
+
const t = setTimeout(() => {
|
|
1570
|
+
if (run.aborted) return;
|
|
1571
|
+
assistantMsg.text += delta;
|
|
1572
|
+
emit({ type: 'assistant_delta', sessionId, delta });
|
|
1573
|
+
if (idx === chunks.length - 1) {
|
|
1574
|
+
activeRuns.delete(sessionId);
|
|
1575
|
+
emit({ type: 'assistant_end', sessionId, message: clone(assistantMsg) });
|
|
1576
|
+
}
|
|
1577
|
+
}, 50 + idx * 40);
|
|
1578
|
+
run.timers.push(t);
|
|
1579
|
+
});
|
|
1580
|
+
|
|
1581
|
+
return { ok: true };
|
|
1582
|
+
};
|
|
930
1583
|
|
|
931
1584
|
const events = {
|
|
932
1585
|
subscribe: (filter, fn) => {
|
|
@@ -1021,40 +1674,236 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
|
|
|
1021
1674
|
const entryAbs = resolveInsideDir(pluginDir, entryRel);
|
|
1022
1675
|
if (!isFile(entryAbs)) throw new Error(`module entry not found: ${entryRel}`);
|
|
1023
1676
|
|
|
1024
|
-
const entryUrl = `/plugin/${encodeURIComponent(entryRel).replaceAll('%2F', '/')}`;
|
|
1025
|
-
|
|
1026
|
-
let backendInstance = null;
|
|
1027
|
-
let backendFactory = null;
|
|
1028
|
-
|
|
1029
|
-
const
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1677
|
+
const entryUrl = `/plugin/${encodeURIComponent(entryRel).replaceAll('%2F', '/')}`;
|
|
1678
|
+
|
|
1679
|
+
let backendInstance = null;
|
|
1680
|
+
let backendFactory = null;
|
|
1681
|
+
|
|
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
|
+
}
|
|
1710
|
+
};
|
|
1711
|
+
|
|
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
|
+
};
|
|
1748
|
+
|
|
1749
|
+
const getSandboxLlmConfig = () => ({ ...sandboxLlmConfig });
|
|
1750
|
+
|
|
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
|
+
};
|
|
1770
|
+
|
|
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
|
+
}
|
|
1782
|
+
|
|
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
|
+
}
|
|
1859
|
+
|
|
1860
|
+
throw new Error('Too many tool calls. Aborting.');
|
|
1861
|
+
};
|
|
1862
|
+
|
|
1863
|
+
const ctxBase = {
|
|
1864
|
+
pluginId: String(manifest?.id || ''),
|
|
1865
|
+
pluginDir,
|
|
1866
|
+
stateDir: path.join(sandboxRoot, 'state', 'chatos'),
|
|
1033
1867
|
sessionRoot: process.cwd(),
|
|
1034
|
-
projectRoot: process.cwd(),
|
|
1035
|
-
dataDir: '',
|
|
1036
|
-
llm: {
|
|
1037
|
-
complete: async (payload) => {
|
|
1038
|
-
const input = typeof payload?.input === 'string' ? payload.input : '';
|
|
1039
|
-
const normalized = String(input || '').trim();
|
|
1040
|
-
if (!normalized) throw new Error('input is required');
|
|
1041
|
-
const
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1868
|
+
projectRoot: process.cwd(),
|
|
1869
|
+
dataDir: '',
|
|
1870
|
+
llm: {
|
|
1871
|
+
complete: async (payload) => {
|
|
1872
|
+
const input = typeof payload?.input === 'string' ? payload.input : '';
|
|
1873
|
+
const normalized = String(input || '').trim();
|
|
1874
|
+
if (!normalized) throw new Error('input is required');
|
|
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
|
+
});
|
|
1882
|
+
return {
|
|
1883
|
+
ok: true,
|
|
1884
|
+
model: result.model,
|
|
1885
|
+
content: result.content,
|
|
1886
|
+
toolTrace: result.toolTrace,
|
|
1887
|
+
};
|
|
1888
|
+
},
|
|
1889
|
+
},
|
|
1890
|
+
};
|
|
1891
|
+
ctxBase.dataDir = path.join(sandboxRoot, 'data', ctxBase.pluginId);
|
|
1892
|
+
ensureDir(ctxBase.stateDir);
|
|
1893
|
+
ensureDir(ctxBase.dataDir);
|
|
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
|
+
});
|
|
1058
1907
|
|
|
1059
1908
|
const sseClients = new Set();
|
|
1060
1909
|
const sseWrite = (res, event, data) => {
|
|
@@ -1079,13 +1928,14 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
|
|
|
1079
1928
|
if (base === '.DS_Store') return;
|
|
1080
1929
|
if (base.endsWith('.map')) return;
|
|
1081
1930
|
|
|
1082
|
-
changeSeq += 1;
|
|
1083
|
-
if (rel.startsWith('backend/')) {
|
|
1084
|
-
backendInstance = null;
|
|
1085
|
-
backendFactory = null;
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
|
|
1931
|
+
changeSeq += 1;
|
|
1932
|
+
if (rel.startsWith('backend/')) {
|
|
1933
|
+
backendInstance = null;
|
|
1934
|
+
backendFactory = null;
|
|
1935
|
+
}
|
|
1936
|
+
resetMcpRuntime().catch(() => {});
|
|
1937
|
+
sseBroadcast('reload', { seq: changeSeq, eventType: eventType || '', path: rel });
|
|
1938
|
+
});
|
|
1089
1939
|
|
|
1090
1940
|
const server = http.createServer(async (req, res) => {
|
|
1091
1941
|
try {
|
|
@@ -1122,16 +1972,27 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
|
|
|
1122
1972
|
return;
|
|
1123
1973
|
}
|
|
1124
1974
|
|
|
1125
|
-
if (req.method === 'GET' && pathname === '/sandbox.mjs') {
|
|
1126
|
-
const tokenNames = loadTokenNames();
|
|
1127
|
-
const
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
.
|
|
1131
|
-
|
|
1132
|
-
.
|
|
1133
|
-
|
|
1134
|
-
|
|
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
|
+
};
|
|
1987
|
+
const js = sandboxClientJs()
|
|
1988
|
+
.replaceAll('__SANDBOX__.context', JSON.stringify(sandboxContext))
|
|
1989
|
+
.replaceAll('__SANDBOX__.pluginId', JSON.stringify(ctxBase.pluginId))
|
|
1990
|
+
.replaceAll('__SANDBOX__.appId', JSON.stringify(effectiveAppId))
|
|
1991
|
+
.replaceAll('__SANDBOX__.entryUrl', JSON.stringify(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));
|
|
1994
|
+
return sendText(res, 200, js, 'text/javascript; charset=utf-8');
|
|
1995
|
+
}
|
|
1135
1996
|
|
|
1136
1997
|
if (req.method === 'GET' && pathname.startsWith('/plugin/')) {
|
|
1137
1998
|
const rel = decodeURIComponent(pathname.slice('/plugin/'.length));
|
|
@@ -1140,15 +2001,77 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
|
|
|
1140
2001
|
return;
|
|
1141
2002
|
}
|
|
1142
2003
|
|
|
1143
|
-
if (req.method === 'GET' && pathname === '/api/manifest') {
|
|
1144
|
-
return sendJson(res, 200, { ok: true, manifest });
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
if (pathname === '/api/
|
|
1148
|
-
if (req.method
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
2004
|
+
if (req.method === 'GET' && pathname === '/api/manifest') {
|
|
2005
|
+
return sendJson(res, 200, { ok: true, manifest });
|
|
2006
|
+
}
|
|
2007
|
+
|
|
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' });
|
|
2045
|
+
}
|
|
2046
|
+
|
|
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
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
if (pathname === '/api/backend/invoke') {
|
|
2071
|
+
if (req.method !== 'POST') return sendJson(res, 405, { ok: false, message: 'Method not allowed' });
|
|
2072
|
+
let body = '';
|
|
2073
|
+
req.on('data', (chunk) => {
|
|
2074
|
+
body += chunk;
|
|
1152
2075
|
});
|
|
1153
2076
|
req.on('end', async () => {
|
|
1154
2077
|
try {
|
|
@@ -1179,7 +2102,10 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
|
|
|
1179
2102
|
sendJson(res, 500, { ok: false, message: e?.message || String(e) });
|
|
1180
2103
|
}
|
|
1181
2104
|
});
|
|
1182
|
-
server.once('close', () =>
|
|
2105
|
+
server.once('close', () => {
|
|
2106
|
+
stopWatch();
|
|
2107
|
+
resetMcpRuntime().catch(() => {});
|
|
2108
|
+
});
|
|
1183
2109
|
|
|
1184
2110
|
await new Promise((resolve, reject) => {
|
|
1185
2111
|
server.once('error', reject);
|