aws-runtime-bridge 1.4.0 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/adapter/AdapterRegistry.d.ts +1 -1
- package/dist/adapter/AdapterRegistry.d.ts.map +1 -1
- package/dist/adapter/AdapterRegistry.js +0 -2
- package/dist/adapter/ClaudeSdkAdapter.d.ts +4 -0
- package/dist/adapter/ClaudeSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/ClaudeSdkAdapter.js +11 -2
- package/dist/adapter/CodexSdkAdapter.js +1 -1
- package/dist/adapter/OpencodeSdkAdapter.d.ts +13 -1
- package/dist/adapter/OpencodeSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/OpencodeSdkAdapter.js +58 -6
- package/dist/adapter/OpencodeSdkAdapter.test.js +57 -1
- package/dist/adapter/types.d.ts +10 -0
- package/dist/adapter/types.d.ts.map +1 -1
- package/dist/index.js +14 -43
- package/dist/middleware/auth.d.ts +5 -0
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +9 -1
- package/dist/routes/file-browser.d.ts +10 -0
- package/dist/routes/file-browser.d.ts.map +1 -1
- package/dist/routes/file-browser.js +226 -4
- package/dist/routes/file-browser.test.js +31 -0
- package/dist/routes/instance.d.ts +10 -0
- package/dist/routes/instance.d.ts.map +1 -1
- package/dist/routes/instance.js +93 -2
- package/dist/routes/instance.test.js +50 -0
- package/dist/routes/pty.d.ts +106 -0
- package/dist/routes/pty.d.ts.map +1 -0
- package/dist/routes/pty.js +526 -0
- package/dist/routes/pty.test.d.ts +2 -0
- package/dist/routes/pty.test.d.ts.map +1 -0
- package/dist/routes/pty.test.js +73 -0
- package/dist/routes/sessions.d.ts +1 -1
- package/dist/routes/sessions.d.ts.map +1 -1
- package/dist/routes/sessions.js +32 -213
- package/dist/routes/terminal.d.ts +32 -3
- package/dist/routes/terminal.d.ts.map +1 -1
- package/dist/routes/terminal.js +411 -243
- package/dist/routes/terminal.test.js +105 -29
- package/dist/services/agent-process-manager.d.ts +2 -2
- package/dist/services/agent-process-manager.d.ts.map +1 -1
- package/dist/services/agent-process-manager.js +3 -3
- package/dist/services/process-detector.d.ts +2 -4
- package/dist/services/process-detector.d.ts.map +1 -1
- package/dist/services/process-detector.js +9 -16
- package/dist/services/process-registry.d.ts +2 -2
- package/dist/services/process-registry.d.ts.map +1 -1
- package/dist/services/process-registry.js +1 -1
- package/dist/services/session-output.d.ts +27 -5
- package/dist/services/session-output.d.ts.map +1 -1
- package/dist/services/session-output.js +48 -3
- package/dist/services/session-output.test.js +43 -29
- package/dist/services/terminal-persistence.d.ts +9 -0
- package/dist/services/terminal-persistence.d.ts.map +1 -1
- package/dist/services/terminal-persistence.js +20 -0
- package/dist/services/tool-installer.d.ts +10 -0
- package/dist/services/tool-installer.d.ts.map +1 -1
- package/dist/services/tool-installer.js +126 -5
- package/dist/services/tool-installer.test.js +32 -1
- package/dist/services/workspace-files.d.ts +86 -0
- package/dist/services/workspace-files.d.ts.map +1 -1
- package/dist/services/workspace-files.js +571 -21
- package/dist/services/workspace-files.test.js +471 -11
- package/dist/services/workspace-watch.d.ts +21 -0
- package/dist/services/workspace-watch.d.ts.map +1 -0
- package/dist/services/workspace-watch.js +123 -0
- package/dist/services/workspace-watch.test.d.ts +2 -0
- package/dist/services/workspace-watch.test.d.ts.map +1 -0
- package/dist/services/workspace-watch.test.js +38 -0
- package/dist/types.d.ts +8 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +9 -1
|
@@ -3,15 +3,79 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 提供文件系统浏览功能
|
|
5
5
|
*/
|
|
6
|
-
import
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import { createReadStream, promises as fs } from 'node:fs';
|
|
7
8
|
import path from 'node:path';
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
9
|
+
import { Router } from 'express';
|
|
10
|
+
import multer from 'multer';
|
|
10
11
|
import { allowHostFileBrowser } from '../config.js';
|
|
12
|
+
import { validateToken } from '../middleware/auth.js';
|
|
11
13
|
import { createLogger } from '../utils/logger.js';
|
|
12
|
-
import { createWorkspaceEntry, deleteWorkspaceEntry, listWorkspaceDirectory, readWorkspaceFile, renameWorkspaceEntry, writeWorkspaceFile, } from '../services/workspace-files.js';
|
|
14
|
+
import { createWorkspaceEntry, deleteWorkspaceEntry, extractWorkspaceArchive, listWorkspaceDirectory, moveWorkspaceEntry, previewWorkspaceDocument, readWorkspaceFile, renameWorkspaceEntry, resolveWorkspaceDownloadTarget, streamWorkspaceDirectoryZip, uploadWorkspaceFiles, writeWorkspaceFile, } from '../services/workspace-files.js';
|
|
15
|
+
import { unwatchWorkspaceFile, watchWorkspaceFile } from '../services/workspace-watch.js';
|
|
13
16
|
const log = createLogger('file-browser');
|
|
14
17
|
export const fileBrowserRouter = Router();
|
|
18
|
+
export const WORKSPACE_UPLOAD_FILE_LIMIT = 2000;
|
|
19
|
+
const upload = multer({
|
|
20
|
+
dest: path.join(os.tmpdir(), 'agentswork-runtime-bridge-uploads'),
|
|
21
|
+
limits: {
|
|
22
|
+
files: WORKSPACE_UPLOAD_FILE_LIMIT,
|
|
23
|
+
fileSize: 512 * 1024 * 1024
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
function toBoolean(value) {
|
|
27
|
+
return value === true || value === 'true' || value === '1';
|
|
28
|
+
}
|
|
29
|
+
function contentDispositionFileName(fileName) {
|
|
30
|
+
const fallbackName = fileName
|
|
31
|
+
.replace(/[\\"\r\n]/g, '_')
|
|
32
|
+
.replace(/[^\x20-\x7E]/g, '_');
|
|
33
|
+
return `attachment; filename="${fallbackName}"; filename*=UTF-8''${encodeURIComponent(fileName)}`;
|
|
34
|
+
}
|
|
35
|
+
async function cleanupUploadedFiles(files = []) {
|
|
36
|
+
await Promise.all(files.map((file) => fs.rm(file.path, { force: true }).catch(() => undefined)));
|
|
37
|
+
}
|
|
38
|
+
export function createWorkspaceUploadLimitResponse(error) {
|
|
39
|
+
return {
|
|
40
|
+
status: 400,
|
|
41
|
+
body: {
|
|
42
|
+
error: error.code === 'LIMIT_FILE_COUNT'
|
|
43
|
+
? `一次最多上传 ${WORKSPACE_UPLOAD_FILE_LIMIT} 个文件,请拆分文件夹后重试。`
|
|
44
|
+
: error.message,
|
|
45
|
+
code: error.code
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function parseWorkspaceUploadRelativePaths(body) {
|
|
50
|
+
const relativePathsJson = body.relativePathsJson;
|
|
51
|
+
if (typeof relativePathsJson === 'string' && relativePathsJson.trim()) {
|
|
52
|
+
const parsed = JSON.parse(relativePathsJson);
|
|
53
|
+
if (!Array.isArray(parsed)) {
|
|
54
|
+
throw new Error('relativePathsJson must be an array');
|
|
55
|
+
}
|
|
56
|
+
return parsed.map((relativePath) => String(relativePath || ''));
|
|
57
|
+
}
|
|
58
|
+
const relativePaths = body.relativePaths;
|
|
59
|
+
return Array.isArray(relativePaths)
|
|
60
|
+
? relativePaths.map((relativePath) => String(relativePath || ''))
|
|
61
|
+
: typeof relativePaths === 'string'
|
|
62
|
+
? [relativePaths]
|
|
63
|
+
: [];
|
|
64
|
+
}
|
|
65
|
+
function workspaceUploadMiddleware(req, res, next) {
|
|
66
|
+
upload.array('files')(req, res, (error) => {
|
|
67
|
+
if (!error) {
|
|
68
|
+
next();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (error instanceof multer.MulterError) {
|
|
72
|
+
const response = createWorkspaceUploadLimitResponse(error);
|
|
73
|
+
res.status(response.status).json(response.body);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
next(error);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
15
79
|
async function listHostDirectories(requestPath = '') {
|
|
16
80
|
const platform = process.platform;
|
|
17
81
|
const isWindows = platform === 'win32';
|
|
@@ -155,6 +219,61 @@ fileBrowserRouter.post('/workspace/read', validateToken, async (req, res) => {
|
|
|
155
219
|
res.status(400).json({ error: err.message });
|
|
156
220
|
}
|
|
157
221
|
});
|
|
222
|
+
/**
|
|
223
|
+
* 预览工作区文档内容。
|
|
224
|
+
* POST /api/file-browser/workspace/preview
|
|
225
|
+
*/
|
|
226
|
+
fileBrowserRouter.post('/workspace/preview', validateToken, async (req, res) => {
|
|
227
|
+
try {
|
|
228
|
+
const { workspacePath, filePath } = req.body || {};
|
|
229
|
+
const result = await previewWorkspaceDocument({
|
|
230
|
+
workspacePath: String(workspacePath || ''),
|
|
231
|
+
filePath: String(filePath || '')
|
|
232
|
+
});
|
|
233
|
+
res.json(result);
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
const err = error;
|
|
237
|
+
log.error('failed to preview workspace document:', err);
|
|
238
|
+
res.status(400).json({ error: err.message });
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
/**
|
|
242
|
+
* 监听工作区文件变化,用于文档预览自动刷新。
|
|
243
|
+
* POST /api/file-browser/workspace/watch
|
|
244
|
+
*/
|
|
245
|
+
fileBrowserRouter.post('/workspace/watch', validateToken, async (req, res) => {
|
|
246
|
+
try {
|
|
247
|
+
const { workspacePath, filePath } = req.body || {};
|
|
248
|
+
res.json(await watchWorkspaceFile({
|
|
249
|
+
workspacePath: String(workspacePath || ''),
|
|
250
|
+
filePath: String(filePath || '')
|
|
251
|
+
}));
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
const err = error;
|
|
255
|
+
log.error('failed to watch workspace file:', err);
|
|
256
|
+
res.status(400).json({ error: err.message });
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
/**
|
|
260
|
+
* 取消监听工作区文件变化。
|
|
261
|
+
* POST /api/file-browser/workspace/unwatch
|
|
262
|
+
*/
|
|
263
|
+
fileBrowserRouter.post('/workspace/unwatch', validateToken, async (req, res) => {
|
|
264
|
+
try {
|
|
265
|
+
const { workspacePath, filePath } = req.body || {};
|
|
266
|
+
res.json(await unwatchWorkspaceFile({
|
|
267
|
+
workspacePath: String(workspacePath || ''),
|
|
268
|
+
filePath: String(filePath || '')
|
|
269
|
+
}));
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
const err = error;
|
|
273
|
+
log.error('failed to unwatch workspace file:', err);
|
|
274
|
+
res.status(400).json({ error: err.message });
|
|
275
|
+
}
|
|
276
|
+
});
|
|
158
277
|
/**
|
|
159
278
|
* 保存工作区文件内容
|
|
160
279
|
* POST /api/file-browser/workspace/write
|
|
@@ -216,6 +335,26 @@ fileBrowserRouter.post('/workspace/rename', validateToken, async (req, res) => {
|
|
|
216
335
|
res.status(400).json({ error: err.message });
|
|
217
336
|
}
|
|
218
337
|
});
|
|
338
|
+
/**
|
|
339
|
+
* 移动工作区中的文件或目录到另一个工作区目录
|
|
340
|
+
* POST /api/file-browser/workspace/move
|
|
341
|
+
*/
|
|
342
|
+
fileBrowserRouter.post('/workspace/move', validateToken, async (req, res) => {
|
|
343
|
+
try {
|
|
344
|
+
const { workspacePath, targetPath, destinationPath } = req.body || {};
|
|
345
|
+
const result = await moveWorkspaceEntry({
|
|
346
|
+
workspacePath: String(workspacePath || ''),
|
|
347
|
+
targetPath: String(targetPath || ''),
|
|
348
|
+
destinationPath: String(destinationPath || '')
|
|
349
|
+
});
|
|
350
|
+
res.json(result);
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
const err = error;
|
|
354
|
+
log.error('failed to move workspace entry:', err);
|
|
355
|
+
res.status(400).json({ error: err.message });
|
|
356
|
+
}
|
|
357
|
+
});
|
|
219
358
|
/**
|
|
220
359
|
* 删除工作区中的文件或目录
|
|
221
360
|
* POST /api/file-browser/workspace/delete
|
|
@@ -235,3 +374,86 @@ fileBrowserRouter.post('/workspace/delete', validateToken, async (req, res) => {
|
|
|
235
374
|
res.status(400).json({ error: err.message });
|
|
236
375
|
}
|
|
237
376
|
});
|
|
377
|
+
/**
|
|
378
|
+
* 上传文件到工作区指定目录,可选自动解压常见压缩包。
|
|
379
|
+
* POST /api/file-browser/workspace/upload
|
|
380
|
+
*/
|
|
381
|
+
fileBrowserRouter.post('/workspace/upload', validateToken, workspaceUploadMiddleware, async (req, res) => {
|
|
382
|
+
const files = (req.files || []);
|
|
383
|
+
try {
|
|
384
|
+
const { workspacePath, path: requestPath = '', extractArchives } = req.body || {};
|
|
385
|
+
const relativePaths = parseWorkspaceUploadRelativePaths(req.body || {});
|
|
386
|
+
const result = await uploadWorkspaceFiles({
|
|
387
|
+
workspacePath: String(workspacePath || ''),
|
|
388
|
+
targetPath: String(requestPath || ''),
|
|
389
|
+
extractArchives: toBoolean(extractArchives),
|
|
390
|
+
files: files.map((file, index) => ({
|
|
391
|
+
originalname: file.originalname,
|
|
392
|
+
relativePath: relativePaths[index],
|
|
393
|
+
path: file.path,
|
|
394
|
+
size: file.size
|
|
395
|
+
}))
|
|
396
|
+
});
|
|
397
|
+
res.json(result);
|
|
398
|
+
}
|
|
399
|
+
catch (error) {
|
|
400
|
+
const err = error;
|
|
401
|
+
log.error('failed to upload workspace files:', err);
|
|
402
|
+
res.status(400).json({ error: err.message });
|
|
403
|
+
}
|
|
404
|
+
finally {
|
|
405
|
+
await cleanupUploadedFiles(files);
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
/**
|
|
409
|
+
* 下载工作区文件或目录;目录会实时打包为 zip。
|
|
410
|
+
* POST /api/file-browser/workspace/download
|
|
411
|
+
*/
|
|
412
|
+
fileBrowserRouter.post('/workspace/download', validateToken, async (req, res) => {
|
|
413
|
+
try {
|
|
414
|
+
const { workspacePath, targetPath } = req.body || {};
|
|
415
|
+
const target = await resolveWorkspaceDownloadTarget({
|
|
416
|
+
workspacePath: String(workspacePath || ''),
|
|
417
|
+
targetPath: String(targetPath || '')
|
|
418
|
+
});
|
|
419
|
+
res.setHeader('Content-Type', target.contentType);
|
|
420
|
+
res.setHeader('Content-Disposition', contentDispositionFileName(target.fileName));
|
|
421
|
+
res.setHeader('X-Workspace-Path', encodeURIComponent(target.workspacePath));
|
|
422
|
+
res.setHeader('X-Workspace-Target-Path', encodeURIComponent(target.targetPath));
|
|
423
|
+
if (target.isDirectory) {
|
|
424
|
+
await streamWorkspaceDirectoryZip(target.resolvedTargetPath, res);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
createReadStream(target.resolvedTargetPath).pipe(res);
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
const err = error;
|
|
431
|
+
log.error('failed to download workspace entry:', err);
|
|
432
|
+
if (!res.headersSent) {
|
|
433
|
+
res.status(400).json({ error: err.message });
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
res.destroy(err);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
/**
|
|
441
|
+
* 解压工作区内的压缩包到指定目录。
|
|
442
|
+
* POST /api/file-browser/workspace/extract
|
|
443
|
+
*/
|
|
444
|
+
fileBrowserRouter.post('/workspace/extract', validateToken, async (req, res) => {
|
|
445
|
+
try {
|
|
446
|
+
const { workspacePath, archivePath, outputPath = '' } = req.body || {};
|
|
447
|
+
const result = await extractWorkspaceArchive({
|
|
448
|
+
workspacePath: String(workspacePath || ''),
|
|
449
|
+
archivePath: String(archivePath || ''),
|
|
450
|
+
outputPath: String(outputPath || '')
|
|
451
|
+
});
|
|
452
|
+
res.json(result);
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
const err = error;
|
|
456
|
+
log.error('failed to extract workspace archive:', err);
|
|
457
|
+
res.status(400).json({ error: err.message });
|
|
458
|
+
}
|
|
459
|
+
});
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* File Browser 路由单元测试
|
|
3
3
|
*/
|
|
4
4
|
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import multer from 'multer';
|
|
6
|
+
import { createWorkspaceUploadLimitResponse, parseWorkspaceUploadRelativePaths, WORKSPACE_UPLOAD_FILE_LIMIT } from './file-browser.js';
|
|
5
7
|
describe('file-browser route validation', () => {
|
|
6
8
|
it('returns root drives on Windows when path is empty', () => {
|
|
7
9
|
const getRootForWindows = () => {
|
|
@@ -61,6 +63,15 @@ describe('file-browser route validation', () => {
|
|
|
61
63
|
expect(validateDeleteRequest({}).valid).toBe(false);
|
|
62
64
|
expect(validateDeleteRequest({ workspacePath: '/p', targetPath: '/p/file' }).valid).toBe(true);
|
|
63
65
|
});
|
|
66
|
+
it('validates move request', () => {
|
|
67
|
+
const validateMoveRequest = (body) => {
|
|
68
|
+
if (!body.workspacePath || !body.targetPath || !body.destinationPath)
|
|
69
|
+
return { valid: false, error: 'workspacePath, targetPath, and destinationPath are required' };
|
|
70
|
+
return { valid: true };
|
|
71
|
+
};
|
|
72
|
+
expect(validateMoveRequest({}).valid).toBe(false);
|
|
73
|
+
expect(validateMoveRequest({ workspacePath: '/p', targetPath: '/p/file', destinationPath: '/p/dest' }).valid).toBe(true);
|
|
74
|
+
});
|
|
64
75
|
});
|
|
65
76
|
describe('file-browser response building', () => {
|
|
66
77
|
it('builds correct list response', () => {
|
|
@@ -85,4 +96,24 @@ describe('file-browser response building', () => {
|
|
|
85
96
|
expect(directories).toEqual(['src']);
|
|
86
97
|
expect(files).toEqual(['package.json']);
|
|
87
98
|
});
|
|
99
|
+
it('returns JSON guidance when folder upload exceeds file count limit', () => {
|
|
100
|
+
const response = createWorkspaceUploadLimitResponse(new multer.MulterError('LIMIT_FILE_COUNT'));
|
|
101
|
+
expect(response.status).toBe(400);
|
|
102
|
+
expect(response.body).toEqual({
|
|
103
|
+
error: `一次最多上传 ${WORKSPACE_UPLOAD_FILE_LIMIT} 个文件,请拆分文件夹后重试。`,
|
|
104
|
+
code: 'LIMIT_FILE_COUNT'
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
it('parses relative paths from a JSON manifest to preserve folder hierarchy', () => {
|
|
108
|
+
const relativePaths = parseWorkspaceUploadRelativePaths({
|
|
109
|
+
relativePathsJson: JSON.stringify(['a/b/c.txt', 'a/d/e.txt'])
|
|
110
|
+
});
|
|
111
|
+
expect(relativePaths).toEqual(['a/b/c.txt', 'a/d/e.txt']);
|
|
112
|
+
});
|
|
113
|
+
it('keeps backward compatibility with repeated relativePaths fields', () => {
|
|
114
|
+
const relativePaths = parseWorkspaceUploadRelativePaths({
|
|
115
|
+
relativePaths: ['a/b/c.txt', 'a/d/e.txt']
|
|
116
|
+
});
|
|
117
|
+
expect(relativePaths).toEqual(['a/b/c.txt', 'a/d/e.txt']);
|
|
118
|
+
});
|
|
88
119
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ToolInstallStatus } from "../types.js";
|
|
1
2
|
export declare function setGracefulShutdownFn(fn: (signal: string, preserveSessions: boolean) => Promise<void>): void;
|
|
2
3
|
export declare const instanceRouter: import("express-serve-static-core").Router;
|
|
3
4
|
export declare function buildConnectionCheckResponse(connectionKeyMd5: string): {
|
|
@@ -37,5 +38,14 @@ export declare function buildSchedulerPingFailureResponse(error: Error, configur
|
|
|
37
38
|
* Main flow: never silently pick the first target; require an explicit scheduler URL or future scheduler identity routing.
|
|
38
39
|
*/
|
|
39
40
|
export declare function buildAmbiguousSchedulerBaseUrlResponse(targetUrls: string[]): AmbiguousSchedulerBaseUrlResponse;
|
|
41
|
+
/**
|
|
42
|
+
* 构建工具状态检测目标列表。
|
|
43
|
+
* 主流程:合并实例启用工具与配置面板固定展示工具,避免历史状态缺少 Codex 时前端误判为待检测。
|
|
44
|
+
*/
|
|
45
|
+
export declare function buildToolStatusDetectionTargets(enabledTools: string[]): string[];
|
|
46
|
+
/**
|
|
47
|
+
* 汇总卸载后仍可执行的工具,用于避免把部分卸载误报为成功。
|
|
48
|
+
*/
|
|
49
|
+
export declare function buildUninstallFailureMessage(requestedTools: string[], toolStatus: Record<string, ToolInstallStatus>): string;
|
|
40
50
|
export {};
|
|
41
51
|
//# sourceMappingURL=instance.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"instance.d.ts","sourceRoot":"","sources":["../../src/routes/instance.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"instance.d.ts","sourceRoot":"","sources":["../../src/routes/instance.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAYrD,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,GAC/D,IAAI,CAEN;AAED,eAAO,MAAM,cAAc,4CAAW,CAAC;AAyBvC,wBAAgB,4BAA4B,CAAC,gBAAgB,EAAE,MAAM,GAAG;IACtE,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE;QACJ,EAAE,EAAE,OAAO,CAAC;QACZ,aAAa,CAAC,EAAE,SAAS,CAAC;QAC1B,oBAAoB,EAAE,OAAO,CAAC;QAC9B,qBAAqB,EAAE,OAAO,CAAC;QAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;CACH,CAqCA;AAED,MAAM,WAAW,4BAA4B;IAC3C,EAAE,EAAE,KAAK,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,gBAAgB,CAAC;IAC/B,aAAa,EAAE,SAAS,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,UAAU,iCAAiC;IACzC,EAAE,EAAE,KAAK,CAAC;IACV,KAAK,EAAE,8BAA8B,CAAC;IACtC,YAAY,EAAE,gBAAgB,CAAC;IAC/B,aAAa,EAAE,SAAS,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;IACzB,sBAAsB,EAAE,MAAM,EAAE,CAAC;IACjC,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,wBAAgB,iCAAiC,CAC/C,KAAK,EAAE,KAAK,EACZ,0BAA0B,EAAE,MAAM,GACjC,4BAA4B,CAiB9B;AAED;;;GAGG;AACH,wBAAgB,sCAAsC,CACpD,UAAU,EAAE,MAAM,EAAE,GACnB,iCAAiC,CAUnC;AAED;;;GAGG;AACH,wBAAgB,+BAA+B,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAQhF;AAED;;GAEG;AACH,wBAAgB,4BAA4B,CAC1C,cAAc,EAAE,MAAM,EAAE,EACxB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,GAC5C,MAAM,CAaR"}
|
package/dist/routes/instance.js
CHANGED
|
@@ -8,10 +8,11 @@ import { loadInstanceState, saveInstanceState, } from "../services/instance-stat
|
|
|
8
8
|
import { initInstance } from "../services/instance-init-service.js";
|
|
9
9
|
import { discoverCcSwitchConfiguredItems, loadCcSwitchSdk } from "../services/cc-switch-sdk.js";
|
|
10
10
|
import { syncLegacyStateFromSdk } from "../services/instance-init-service.js";
|
|
11
|
-
import { detectToolStatuses, ensureToolsInstalled, SUPPORTED_INSTALLABLE_TOOLS, } from "../services/tool-installer.js";
|
|
11
|
+
import { detectToolStatuses, ensureToolsInstalled, SUPPORTED_INSTALLABLE_TOOLS, SUPPORTED_UNINSTALLABLE_TOOLS, uninstallTools, } from "../services/tool-installer.js";
|
|
12
12
|
import { getConfiguredConnectionKeys } from "../services/auto-register.js";
|
|
13
13
|
import { createLogger } from "../utils/logger.js";
|
|
14
14
|
const log = createLogger("instance");
|
|
15
|
+
const PANEL_TOOL_STATUS_KEYS = ["claude", "opencode", "codex"];
|
|
15
16
|
// ★★★ 导出 gracefulShutdown 的触发函数
|
|
16
17
|
// 用于 aws-mcp-server 通知 bridge 保留会话状态关闭
|
|
17
18
|
let gracefulShutdownFn = null;
|
|
@@ -106,6 +107,31 @@ export function buildAmbiguousSchedulerBaseUrlResponse(targetUrls) {
|
|
|
106
107
|
hint: "Multiple autoRegisterTargets.serverUrl values were found, so bridge cannot safely infer which scheduler /runtime/ping should call back. Set AWS_RUNTIME_SCHEDULER_BASE_URL explicitly, or use future schedulerId/token-bound multi-scheduler routing.",
|
|
107
108
|
};
|
|
108
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* 构建工具状态检测目标列表。
|
|
112
|
+
* 主流程:合并实例启用工具与配置面板固定展示工具,避免历史状态缺少 Codex 时前端误判为待检测。
|
|
113
|
+
*/
|
|
114
|
+
export function buildToolStatusDetectionTargets(enabledTools) {
|
|
115
|
+
return Array.from(new Set([...enabledTools, ...PANEL_TOOL_STATUS_KEYS]
|
|
116
|
+
.map((tool) => String(tool || "").trim().toLowerCase())
|
|
117
|
+
.filter(Boolean)));
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* 汇总卸载后仍可执行的工具,用于避免把部分卸载误报为成功。
|
|
121
|
+
*/
|
|
122
|
+
export function buildUninstallFailureMessage(requestedTools, toolStatus) {
|
|
123
|
+
const failedTools = requestedTools.filter((tool) => toolStatus[tool]?.installed);
|
|
124
|
+
if (failedTools.length === 0) {
|
|
125
|
+
return "";
|
|
126
|
+
}
|
|
127
|
+
return failedTools
|
|
128
|
+
.map((tool) => {
|
|
129
|
+
const status = toolStatus[tool];
|
|
130
|
+
const detail = status?.error || status?.executable || "command is still available";
|
|
131
|
+
return `${tool}: ${detail}`;
|
|
132
|
+
})
|
|
133
|
+
.join("; ");
|
|
134
|
+
}
|
|
109
135
|
instanceRouter.get("/healthz", (_req, res) => {
|
|
110
136
|
res.json({
|
|
111
137
|
ok: true,
|
|
@@ -182,7 +208,7 @@ instanceRouter.post("/cc-switch/state", validateToken, async (req, res) => {
|
|
|
182
208
|
const imported = await discoverCcSwitchConfiguredItems(sdk);
|
|
183
209
|
const synced = await syncLegacyStateFromSdk(agentId, sdk);
|
|
184
210
|
const state = synced.state;
|
|
185
|
-
const toolStatus = await detectToolStatuses(state.enabledTools || []);
|
|
211
|
+
const toolStatus = await detectToolStatuses(buildToolStatusDetectionTargets(state.enabledTools || []));
|
|
186
212
|
res.json({
|
|
187
213
|
ok: true,
|
|
188
214
|
agentId: String(agentId),
|
|
@@ -247,6 +273,71 @@ instanceRouter.post("/cc-switch/install-tools", validateToken, async (req, res)
|
|
|
247
273
|
res.status(400).json({ error: err?.message || "install tools failed" });
|
|
248
274
|
}
|
|
249
275
|
});
|
|
276
|
+
instanceRouter.post("/cc-switch/uninstall-tools", validateToken, async (req, res) => {
|
|
277
|
+
const { agentId, tools } = req.body || {};
|
|
278
|
+
if (!agentId) {
|
|
279
|
+
res.status(400).json({ error: "agentId is required" });
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const requestedTools = Array.isArray(tools)
|
|
283
|
+
? tools
|
|
284
|
+
.map((tool) => String(tool || "")
|
|
285
|
+
.trim()
|
|
286
|
+
.toLowerCase())
|
|
287
|
+
.filter(Boolean)
|
|
288
|
+
: [];
|
|
289
|
+
const supportedUninstallableTools = new Set(SUPPORTED_UNINSTALLABLE_TOOLS);
|
|
290
|
+
const uninstallableTools = requestedTools.filter((tool) => supportedUninstallableTools.has(tool));
|
|
291
|
+
if (uninstallableTools.length === 0) {
|
|
292
|
+
res.status(400).json({
|
|
293
|
+
error: `tools must include ${SUPPORTED_UNINSTALLABLE_TOOLS.join(", ")}`,
|
|
294
|
+
});
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
try {
|
|
298
|
+
const state = await loadInstanceState(agentId);
|
|
299
|
+
const uninstallableToolSet = new Set(uninstallableTools);
|
|
300
|
+
const enabledTools = (state.enabledTools || []).filter((tool) => !uninstallableToolSet.has(String(tool || "").trim().toLowerCase()));
|
|
301
|
+
const uninstalledToolStatus = await uninstallTools(uninstallableTools);
|
|
302
|
+
const refreshedToolStatus = await detectToolStatuses([
|
|
303
|
+
...enabledTools,
|
|
304
|
+
...uninstallableTools,
|
|
305
|
+
]);
|
|
306
|
+
const nextToolStatus = {
|
|
307
|
+
...refreshedToolStatus,
|
|
308
|
+
...uninstalledToolStatus,
|
|
309
|
+
};
|
|
310
|
+
const uninstallFailureMessage = buildUninstallFailureMessage(uninstallableTools, nextToolStatus);
|
|
311
|
+
const nextEnabledTools = uninstallFailureMessage
|
|
312
|
+
? state.enabledTools || []
|
|
313
|
+
: enabledTools;
|
|
314
|
+
const savedState = await saveInstanceState(agentId, {
|
|
315
|
+
...state,
|
|
316
|
+
enabledTools: nextEnabledTools,
|
|
317
|
+
toolStatus: nextToolStatus,
|
|
318
|
+
});
|
|
319
|
+
if (uninstallFailureMessage) {
|
|
320
|
+
res.status(400).json({
|
|
321
|
+
ok: false,
|
|
322
|
+
agentId: String(agentId),
|
|
323
|
+
error: `uninstall incomplete: ${uninstallFailureMessage}`,
|
|
324
|
+
toolStatus: nextToolStatus,
|
|
325
|
+
state: savedState,
|
|
326
|
+
});
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
res.json({
|
|
330
|
+
ok: true,
|
|
331
|
+
agentId: String(agentId),
|
|
332
|
+
toolStatus: nextToolStatus,
|
|
333
|
+
state: savedState,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
const err = error;
|
|
338
|
+
res.status(400).json({ error: err?.message || "uninstall tools failed" });
|
|
339
|
+
}
|
|
340
|
+
});
|
|
250
341
|
/**
|
|
251
342
|
* ★★★ 新增:aws-mcp-server 通知 bridge 准备重启
|
|
252
343
|
* POST /runtime/prepare-restart
|
|
@@ -55,6 +55,14 @@ describe('instance route validation', () => {
|
|
|
55
55
|
expect(validateStateRequest({}).valid).toBe(false);
|
|
56
56
|
expect(validateStateRequest({ agentId: 'agent-1' }).valid).toBe(true);
|
|
57
57
|
});
|
|
58
|
+
it('always detects panel tool statuses even when legacy enabled tools omit codex', async () => {
|
|
59
|
+
const { buildToolStatusDetectionTargets } = await import('./instance.js');
|
|
60
|
+
expect(buildToolStatusDetectionTargets(['claude', 'opencode'])).toEqual([
|
|
61
|
+
'claude',
|
|
62
|
+
'opencode',
|
|
63
|
+
'codex',
|
|
64
|
+
]);
|
|
65
|
+
});
|
|
58
66
|
it('accepts codex for cc-switch tool installation validation', async () => {
|
|
59
67
|
const { SUPPORTED_INSTALLABLE_TOOLS } = await import('../services/tool-installer.js');
|
|
60
68
|
const normalizeInstallableTools = (tools) => {
|
|
@@ -67,6 +75,48 @@ describe('instance route validation', () => {
|
|
|
67
75
|
expect(normalizeInstallableTools(['Codex'])).toEqual(['codex']);
|
|
68
76
|
expect(SUPPORTED_INSTALLABLE_TOOLS).toContain('codex');
|
|
69
77
|
});
|
|
78
|
+
it('accepts panel tools for cc-switch tool uninstallation validation', async () => {
|
|
79
|
+
const { SUPPORTED_UNINSTALLABLE_TOOLS } = await import('../services/tool-installer.js');
|
|
80
|
+
const normalizeUninstallableTools = (tools) => {
|
|
81
|
+
const supported = new Set(SUPPORTED_UNINSTALLABLE_TOOLS);
|
|
82
|
+
const requestedTools = Array.isArray(tools)
|
|
83
|
+
? tools.map((tool) => String(tool || '').trim().toLowerCase()).filter(Boolean)
|
|
84
|
+
: [];
|
|
85
|
+
return requestedTools.filter((tool) => supported.has(tool));
|
|
86
|
+
};
|
|
87
|
+
expect(normalizeUninstallableTools(['Claude', 'OpenCode', 'Codex'])).toEqual([
|
|
88
|
+
'claude',
|
|
89
|
+
'opencode',
|
|
90
|
+
'codex',
|
|
91
|
+
]);
|
|
92
|
+
expect(normalizeUninstallableTools(['unknown-tool'])).toEqual([]);
|
|
93
|
+
});
|
|
94
|
+
it('reports uninstall failure when a requested tool remains installed', async () => {
|
|
95
|
+
const { buildUninstallFailureMessage } = await import('./instance.js');
|
|
96
|
+
expect(buildUninstallFailureMessage(['opencode'], {
|
|
97
|
+
opencode: {
|
|
98
|
+
tool: 'opencode',
|
|
99
|
+
installed: true,
|
|
100
|
+
executable: '/home/user/.opencode/bin/opencode',
|
|
101
|
+
version: '1.14.39',
|
|
102
|
+
installing: false,
|
|
103
|
+
error: 'uninstall completed but command is still available',
|
|
104
|
+
},
|
|
105
|
+
})).toContain('opencode: uninstall completed but command is still available');
|
|
106
|
+
});
|
|
107
|
+
it('does not report uninstall failure when requested tools are no longer installed', async () => {
|
|
108
|
+
const { buildUninstallFailureMessage } = await import('./instance.js');
|
|
109
|
+
expect(buildUninstallFailureMessage(['claude'], {
|
|
110
|
+
claude: {
|
|
111
|
+
tool: 'claude',
|
|
112
|
+
installed: false,
|
|
113
|
+
executable: null,
|
|
114
|
+
version: null,
|
|
115
|
+
installing: false,
|
|
116
|
+
error: 'command not installed',
|
|
117
|
+
},
|
|
118
|
+
})).toBe('');
|
|
119
|
+
});
|
|
70
120
|
it('allows connection check when no connection key is configured', async () => {
|
|
71
121
|
const { getConfiguredConnectionKeys } = await import('../services/auto-register.js');
|
|
72
122
|
vi.mocked(getConfiguredConnectionKeys).mockReturnValue([]);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { IncomingMessage } from "node:http";
|
|
2
|
+
import type { Socket } from "node:net";
|
|
3
|
+
import type { Router } from "express";
|
|
4
|
+
import * as pty from "node-pty";
|
|
5
|
+
import WebSocket from "ws";
|
|
6
|
+
export type PtySessionStatus = "running" | "exited";
|
|
7
|
+
export interface PtySessionSummary {
|
|
8
|
+
id: string;
|
|
9
|
+
title: string;
|
|
10
|
+
workspacePath: string;
|
|
11
|
+
shell: string;
|
|
12
|
+
cols: number;
|
|
13
|
+
rows: number;
|
|
14
|
+
status: PtySessionStatus;
|
|
15
|
+
createdAt: string;
|
|
16
|
+
lastActiveAt: string;
|
|
17
|
+
exitCode?: number;
|
|
18
|
+
signal?: number;
|
|
19
|
+
attachedClients: number;
|
|
20
|
+
ownerUserId?: string;
|
|
21
|
+
tenantId?: string;
|
|
22
|
+
projectName?: string;
|
|
23
|
+
}
|
|
24
|
+
interface PtySessionEntry extends PtySessionSummary {
|
|
25
|
+
ptyProcess: pty.IPty;
|
|
26
|
+
sockets: Set<WebSocket>;
|
|
27
|
+
outputBuffer: string;
|
|
28
|
+
idleTimer: NodeJS.Timeout | null;
|
|
29
|
+
persistent: boolean;
|
|
30
|
+
}
|
|
31
|
+
export declare const ptySessions: Map<string, PtySessionEntry>;
|
|
32
|
+
/**
|
|
33
|
+
* 解析 PTY 空闲清理时间。
|
|
34
|
+
* 主流程:读取环境变量 -> 非法值回退默认值 -> 用于 WebSocket 全断开后的延迟清理。
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolvePtyIdleTtlMs(env?: NodeJS.ProcessEnv): number;
|
|
37
|
+
/**
|
|
38
|
+
* 选择默认 shell。
|
|
39
|
+
* 主流程:Windows 优先 pwsh/powershell/cmd;Unix 优先 SHELL/bash/sh,返回可执行文件名与参数。
|
|
40
|
+
*/
|
|
41
|
+
export declare function resolveDefaultShell(platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv): {
|
|
42
|
+
shell: string;
|
|
43
|
+
args: string[];
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* 校验并解析 shell 选择。
|
|
47
|
+
* 主流程:auto 使用默认 shell;显式 shell 必须命中 allowlist,避免前端任意执行二进制。
|
|
48
|
+
*/
|
|
49
|
+
export declare function resolveRequestedShell(requestedShell: unknown, platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv): {
|
|
50
|
+
shell: string;
|
|
51
|
+
args: string[];
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* 判断候选路径是否位于允许根目录中。
|
|
55
|
+
* 具体逻辑:使用 path.relative 处理 Windows/Unix 分隔符,拒绝向上穿越和跨盘路径。
|
|
56
|
+
*/
|
|
57
|
+
export declare function isPathInsideRoot(candidatePath: string, rootPath: string): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* 解析并校验 PTY 工作目录。
|
|
60
|
+
* 主流程:要求绝对目录存在;如果配置 AWS_PTY_ALLOWED_ROOTS,则必须位于允许根目录内。
|
|
61
|
+
*/
|
|
62
|
+
export declare function resolvePtyWorkspacePath(workspacePath: unknown, env?: NodeJS.ProcessEnv): string;
|
|
63
|
+
/**
|
|
64
|
+
* 创建 PTY session。
|
|
65
|
+
* 主流程:校验 cwd/shell -> spawn 真实 PTY -> 注册 output/exit 事件 -> 返回元数据给控制面。
|
|
66
|
+
*/
|
|
67
|
+
export declare function createPtySession(input: {
|
|
68
|
+
workspacePath: unknown;
|
|
69
|
+
title?: unknown;
|
|
70
|
+
shell?: unknown;
|
|
71
|
+
cols?: unknown;
|
|
72
|
+
rows?: unknown;
|
|
73
|
+
persistent?: unknown;
|
|
74
|
+
preCommand?: unknown;
|
|
75
|
+
ownerUserId?: unknown;
|
|
76
|
+
tenantId?: unknown;
|
|
77
|
+
projectName?: unknown;
|
|
78
|
+
}): PtySessionSummary;
|
|
79
|
+
export declare function resolvePtyExitedTtlMs(env?: NodeJS.ProcessEnv): number;
|
|
80
|
+
export declare function resolveMaxPtySessions(env?: NodeJS.ProcessEnv): number;
|
|
81
|
+
export declare function listPtySessions(): PtySessionSummary[];
|
|
82
|
+
export declare function getPtySessionSummary(id: string): PtySessionSummary | undefined;
|
|
83
|
+
/**
|
|
84
|
+
* 生成短期 PTY 连接令牌。
|
|
85
|
+
* 主流程:确认 session 存在 -> 生成随机 token -> 记录 session 绑定和过期时间。
|
|
86
|
+
*/
|
|
87
|
+
export declare function createPtyConnectToken(sessionId: string): {
|
|
88
|
+
token: string;
|
|
89
|
+
expiresAt: string;
|
|
90
|
+
} | undefined;
|
|
91
|
+
/**
|
|
92
|
+
* 关闭 PTY session。
|
|
93
|
+
* 主流程:关闭连接 -> kill PTY -> 清理定时器和内存索引。
|
|
94
|
+
*/
|
|
95
|
+
export declare function closePtySession(id: string, reason?: string): boolean;
|
|
96
|
+
export declare function closeAllPtySessions(reason?: string): void;
|
|
97
|
+
export declare const ptyRouter: Router;
|
|
98
|
+
/**
|
|
99
|
+
* 将 PTY WebSocket upgrade 挂到 HTTP server。
|
|
100
|
+
* 主流程:匹配 /pty/sessions/:id/stream -> token 校验 -> session 校验 -> 交给 noServer WSS。
|
|
101
|
+
*/
|
|
102
|
+
export declare function attachPtyWebSocketServer(server: {
|
|
103
|
+
on: (event: "upgrade", listener: (request: IncomingMessage, socket: Socket, head: Buffer) => void) => unknown;
|
|
104
|
+
}): void;
|
|
105
|
+
export {};
|
|
106
|
+
//# sourceMappingURL=pty.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pty.d.ts","sourceRoot":"","sources":["../../src/routes/pty.ts"],"names":[],"mappings":"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"}
|