neoctl-web 0.1.12 → 0.1.14
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 +41 -1
- package/chunk-uploads.mjs +125 -0
- package/core-runtime.mjs +1 -0
- package/dist/assets/App--pG9o2Yv.js +105 -0
- package/dist/assets/App-DmtsTXw2.css +1 -0
- package/dist/assets/index-Ckm5fgYu.js +18 -0
- package/dist/assets/index-Dgfhk2Ym.css +1 -0
- package/dist/index.html +2 -2
- package/execution-backend.mjs +74 -0
- package/isolation-auth.mjs +225 -0
- package/isolation.example.json +8 -0
- package/isolation.mjs +385 -0
- package/package.json +9 -2
- package/plugins/downloads/README.md +40 -0
- package/plugins/downloads/downloads.mjs +81 -120
- package/plugins/downloads/downloads.test.mjs +83 -0
- package/plugins/downloads/index.mjs +10 -7
- package/plugins/downloads/neo-plugin.json +2 -2
- package/plugins/video-share/README.md +55 -0
- package/plugins/video-share/http.mjs +86 -0
- package/plugins/video-share/index.mjs +65 -0
- package/plugins/video-share/neo-plugin.json +9 -0
- package/plugins/video-share/presentation.mjs +23 -0
- package/plugins/video-share/store.mjs +76 -0
- package/plugins/video-share/video-share.test.mjs +182 -0
- package/plugins/xhs-artifact/editor-page.mjs +4 -4
- package/runtime-workspaces.mjs +9 -6
- package/scripts/isolation-user.mjs +68 -0
- package/server.mjs +27 -7
- package/tool-settings.mjs +16 -9
- package/dist/assets/index-DEFD25Vs.js +0 -121
- package/dist/assets/index-ernsuQxg.css +0 -1
package/server.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
|
+
import { createChunkUploadHandler } from './chunk-uploads.mjs';
|
|
2
3
|
import fs from 'node:fs';
|
|
3
4
|
import fsp from 'node:fs/promises';
|
|
4
5
|
import path from 'node:path';
|
|
@@ -12,6 +13,8 @@ let desktopControlConfig;
|
|
|
12
13
|
try { desktopControlConfig = raw ? JSON.parse(raw) : undefined; } catch {}
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
const { containerMode, workspaceFs, deliverUpload, verifyExecutionBackend } = await import('./execution-backend.mjs');
|
|
17
|
+
await verifyExecutionBackend();
|
|
15
18
|
const { coreRuntimeInfo, createWebRuntime, loadNeoPlugins, runWebServer } = await import('./core-runtime.mjs');
|
|
16
19
|
const { createWebPluginHost } = await import('./plugins.mjs');
|
|
17
20
|
const { createWebPluginSettings } = await import('./plugin-settings.mjs');
|
|
@@ -37,7 +40,9 @@ const root = path.resolve(process.env.DIST_DIR || path.join(__dirname, 'dist'));
|
|
|
37
40
|
const host = process.env.APP_HOST || '0.0.0.0';
|
|
38
41
|
const port = Number(process.env.APP_PORT || process.env.PORT || 5173);
|
|
39
42
|
const runtimeTarget = new URL(process.env.NEO_RUNTIME_TARGET || 'http://127.0.0.1:3101');
|
|
40
|
-
const
|
|
43
|
+
const storage = resolveWebStorage();
|
|
44
|
+
const dataRoot = storage.dataRoot;
|
|
45
|
+
const workspaceRoot = containerMode ? '/workspace' : storage.workspaceRoot;
|
|
41
46
|
const promptLibraryFile = path.resolve(process.env.NEO_PROMPT_LIBRARY_FILE || path.join(dataRoot, 'prompt-library.json'));
|
|
42
47
|
const uploadsDir = path.resolve(process.env.NEO_UPLOADS_DIR || path.join(dataRoot, 'uploads'));
|
|
43
48
|
const pluginDir = path.resolve(process.env.NEO_WEB_PLUGIN_DIR || path.join(__dirname, 'plugins'));
|
|
@@ -46,7 +51,7 @@ const cpaConfigFile = path.resolve(process.env.NEO_CPA_CONFIG_FILE || path.join(
|
|
|
46
51
|
const memoryMonitorFile = path.resolve(process.env.NEO_MEMORY_MONITOR_FILE || path.join(dataRoot, 'memory-monitor.json'));
|
|
47
52
|
const pluginSettingsFile = path.resolve(process.env.NEO_WEB_PLUGIN_SETTINGS_FILE || path.join(dataRoot, 'plugins.json'));
|
|
48
53
|
const toolSettingsFile = path.resolve(process.env.NEO_WEB_TOOL_SETTINGS_FILE || path.join(dataRoot, 'tools.json'));
|
|
49
|
-
const
|
|
54
|
+
const chunkUploads = createChunkUploadHandler({ uploadsDir, baseDir: __dirname, finalize: containerMode ? (file, url) => deliverUpload(file, url, runtimeTarget) : undefined });
|
|
50
55
|
const pluginSettings = await createWebPluginSettings(pluginSettingsFile);
|
|
51
56
|
const toolSettings = await createWebToolSettings(toolSettingsFile);
|
|
52
57
|
const pluginEnv = process.env.NEO_WEB_PLUGINS;
|
|
@@ -86,6 +91,7 @@ const workspaceRuntime = createWorkspaceRuntimeManager({
|
|
|
86
91
|
...runtimeOptions,
|
|
87
92
|
...pluginHost.runtimePlugins(runtimeOptions.sessionId),
|
|
88
93
|
globalToolOverrides: toolSettings.globalOverrides(),
|
|
94
|
+
resolveGlobalToolOverrides: () => toolSettings.globalOverrides(),
|
|
89
95
|
sessionToolOverrides: toolSettings.sessionOverrides(runtimeOptions.sessionId),
|
|
90
96
|
persistGlobalToolOverrides: (overrides) => toolSettings.setGlobalOverrides(overrides),
|
|
91
97
|
persistSessionToolOverrides: (sessionId, overrides) => toolSettings.setSessionOverrides(sessionId, overrides),
|
|
@@ -93,6 +99,10 @@ const workspaceRuntime = createWorkspaceRuntimeManager({
|
|
|
93
99
|
}),
|
|
94
100
|
});
|
|
95
101
|
|
|
102
|
+
const { createIsolationMode } = await import('./isolation.mjs');
|
|
103
|
+
const isolation = await createIsolationMode({ dataRoot, workspaceRoot, pluginDir, pluginSettings, toolSettings, cpaQuotaMonitor, memoryState: () => memoryMonitor.getPublicState() });
|
|
104
|
+
if (isolation.enabled && controlEnabled) throw new Error('Isolation mode cannot use desktop control sync');
|
|
105
|
+
|
|
96
106
|
const DEFAULT_APP_PROMPT_LIBRARY = [];
|
|
97
107
|
|
|
98
108
|
const mime = {
|
|
@@ -117,9 +127,10 @@ const server = http.createServer((req, res) => {
|
|
|
117
127
|
void routeRequest(req, res);
|
|
118
128
|
});
|
|
119
129
|
|
|
130
|
+
server.once('close', () => isolation.close());
|
|
120
131
|
server.keepAliveTimeout = 70_000;
|
|
121
132
|
server.headersTimeout = 75_000;
|
|
122
|
-
if (embedRuntime) await startEmbeddedRuntime();
|
|
133
|
+
if (embedRuntime && !isolation.enabled) await startEmbeddedRuntime();
|
|
123
134
|
await cpaQuotaMonitor.start();
|
|
124
135
|
await memoryMonitor.start();
|
|
125
136
|
await new Promise((resolve, reject) => {
|
|
@@ -157,6 +168,7 @@ async function startEmbeddedRuntime() {
|
|
|
157
168
|
async function routeRequest(req, res) {
|
|
158
169
|
const url = new URL(req.url || '/', 'http://localhost');
|
|
159
170
|
try {
|
|
171
|
+
if (await isolation.route(req, res, url)) return;
|
|
160
172
|
if (await pluginHost.route(req, res, url, { readJsonBody, sendJson })) return;
|
|
161
173
|
if (req.method === 'GET' && url.pathname === '/api/prompt-library') {
|
|
162
174
|
return sendJson(res, { items: await readPromptLibrary() });
|
|
@@ -212,6 +224,7 @@ async function routeRequest(req, res) {
|
|
|
212
224
|
await writePromptLibrary(ordered);
|
|
213
225
|
return sendJson(res, { ok: true, items: ordered });
|
|
214
226
|
}
|
|
227
|
+
if (await chunkUploads(req, res, url)) return;
|
|
215
228
|
if (req.method === 'GET' && url.pathname.startsWith('/api/uploads/')) {
|
|
216
229
|
const storedName = decodeURIComponent(url.pathname.slice('/api/uploads/'.length));
|
|
217
230
|
return serveUploadedFile(res, storedName);
|
|
@@ -222,7 +235,7 @@ async function routeRequest(req, res) {
|
|
|
222
235
|
}
|
|
223
236
|
if (req.method === 'POST' && url.pathname === '/api/uploads') {
|
|
224
237
|
const body = await readJsonBody(req);
|
|
225
|
-
const file = await storeUploadedFile(body);
|
|
238
|
+
const file = await deliverUpload(await storeUploadedFile(body), url, runtimeTarget);
|
|
226
239
|
return sendJson(res, { ok: true, file });
|
|
227
240
|
}
|
|
228
241
|
if (shouldProxy(url.pathname)) {
|
|
@@ -384,7 +397,6 @@ async function storeUploadedFile(payload) {
|
|
|
384
397
|
if (!data) throw new Error('missing upload data');
|
|
385
398
|
const buffer = Buffer.from(data, 'base64');
|
|
386
399
|
if (!buffer.length) throw new Error('empty upload data');
|
|
387
|
-
if (buffer.length > maxUploadBytes) throw new Error(`upload too large: max ${maxUploadBytes} bytes`);
|
|
388
400
|
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
389
401
|
const random = Math.random().toString(36).slice(2, 8);
|
|
390
402
|
const storedName = `${stamp}-${random}-${name}`;
|
|
@@ -418,12 +430,14 @@ async function serveUploadedFile(res, storedName) {
|
|
|
418
430
|
return;
|
|
419
431
|
}
|
|
420
432
|
try {
|
|
421
|
-
const
|
|
433
|
+
const stat = await fsp.stat(filePath);
|
|
434
|
+
if (!stat.isFile()) throw new Error('not a file');
|
|
422
435
|
res.writeHead(200, {
|
|
436
|
+
'Content-Length': String(stat.size),
|
|
423
437
|
'Content-Type': mime[path.extname(filePath).toLowerCase()] || 'application/octet-stream',
|
|
424
438
|
'Cache-Control': 'public, max-age=31536000, immutable',
|
|
425
439
|
});
|
|
426
|
-
res.
|
|
440
|
+
fs.createReadStream(filePath).on('error', () => res.destroy()).pipe(res);
|
|
427
441
|
} catch {
|
|
428
442
|
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
429
443
|
res.end('not found');
|
|
@@ -447,6 +461,12 @@ async function serveLocalImage(res, encodedPath) {
|
|
|
447
461
|
return;
|
|
448
462
|
}
|
|
449
463
|
try {
|
|
464
|
+
if (containerMode) {
|
|
465
|
+
const body = await workspaceFs.readFile(absolutePath);
|
|
466
|
+
res.writeHead(200, { "Content-Type": contentType, "Content-Length": body.length, "Cache-Control": "no-store" });
|
|
467
|
+
res.end(body);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
450
470
|
const fileStat = await fsp.stat(absolutePath);
|
|
451
471
|
if (!fileStat.isFile()) throw new Error('not a file');
|
|
452
472
|
res.writeHead(200, {
|
package/tool-settings.mjs
CHANGED
|
@@ -9,10 +9,14 @@ export async function createWebToolSettings(storageFile) {
|
|
|
9
9
|
return structuredClone(state);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
async function update(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
async function update(transform) {
|
|
13
|
+
const mutation = writeQueue.catch(() => undefined).then(async () => {
|
|
14
|
+
const next = transform(state);
|
|
15
|
+
await writeState(storageFile, next);
|
|
16
|
+
state = next;
|
|
17
|
+
});
|
|
18
|
+
writeQueue = mutation;
|
|
19
|
+
await mutation;
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
return {
|
|
@@ -25,16 +29,19 @@ export async function createWebToolSettings(storageFile) {
|
|
|
25
29
|
return value && typeof value === 'object' ? { ...value } : {};
|
|
26
30
|
},
|
|
27
31
|
async setGlobalOverrides(overrides) {
|
|
28
|
-
|
|
32
|
+
const global = normalizeOverrides(overrides);
|
|
33
|
+
await update(current => ({ ...current, global }));
|
|
29
34
|
},
|
|
30
35
|
async setSessionOverrides(sessionId, overrides) {
|
|
31
36
|
const id = String(sessionId || '').trim();
|
|
32
37
|
if (!id) throw new Error('session id is required');
|
|
33
|
-
const sessions = { ...state.sessions };
|
|
34
38
|
const normalized = normalizeOverrides(overrides);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
await update(current => {
|
|
40
|
+
const sessions = { ...current.sessions };
|
|
41
|
+
if (Object.keys(normalized).length) sessions[id] = normalized;
|
|
42
|
+
else delete sessions[id];
|
|
43
|
+
return { ...current, sessions };
|
|
44
|
+
});
|
|
38
45
|
},
|
|
39
46
|
};
|
|
40
47
|
}
|