@xcanwin/manyoyo 5.8.10 → 5.9.0
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/lib/web/frontend/app.css +196 -1
- package/lib/web/frontend/app.html +20 -0
- package/lib/web/frontend/app.js +118 -2
- package/lib/web/frontend/codemirror-entry.js +98 -0
- package/lib/web/frontend/codemirror.bundle.js +31648 -0
- package/lib/web/frontend/file-browser.js +406 -0
- package/lib/web/frontend/markdown-renderer.js +189 -28
- package/lib/web/frontend/markdown.css +9 -1
- package/lib/web/server.js +230 -1
- package/package.json +15 -1
|
@@ -72,5 +72,13 @@
|
|
|
72
72
|
|
|
73
73
|
.bubble .md-content a {
|
|
74
74
|
color: #8a4f17;
|
|
75
|
-
text-decoration-
|
|
75
|
+
text-decoration-line: underline;
|
|
76
|
+
text-decoration-thickness: 2px;
|
|
77
|
+
text-underline-offset: 0.16em;
|
|
78
|
+
text-decoration-color: rgba(138, 79, 23, 0.8);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.bubble .md-content a:hover,
|
|
82
|
+
.bubble .md-content a:focus-visible {
|
|
83
|
+
text-decoration-color: #8a4f17;
|
|
76
84
|
}
|
package/lib/web/server.js
CHANGED
|
@@ -34,6 +34,7 @@ const WEB_TERMINAL_MIN_ROWS = 12;
|
|
|
34
34
|
const WEB_AGENT_CONTEXT_MAX_MESSAGES = 24;
|
|
35
35
|
const WEB_AGENT_CONTEXT_MAX_CHARS = 6000;
|
|
36
36
|
const WEB_AGENT_CONTEXT_PER_MESSAGE_MAX_CHARS = 600;
|
|
37
|
+
const WEB_FILE_PREVIEW_MAX_BYTES = 256 * 1024;
|
|
37
38
|
const WEB_AUTH_COOKIE_NAME = 'manyoyo_web_auth';
|
|
38
39
|
const WEB_AUTH_TTL_SECONDS = 12 * 60 * 60;
|
|
39
40
|
const WEB_SESSION_KEY_SEPARATOR = '~';
|
|
@@ -109,6 +110,24 @@ const MIME_TYPES = {
|
|
|
109
110
|
'.js': 'application/javascript; charset=utf-8',
|
|
110
111
|
'.html': 'text/html; charset=utf-8'
|
|
111
112
|
};
|
|
113
|
+
const FILE_LANGUAGE_MAP = {
|
|
114
|
+
'.cjs': 'javascript',
|
|
115
|
+
'.css': 'css',
|
|
116
|
+
'.htm': 'html',
|
|
117
|
+
'.html': 'html',
|
|
118
|
+
'.java': 'javascript',
|
|
119
|
+
'.js': 'javascript',
|
|
120
|
+
'.json': 'json',
|
|
121
|
+
'.jsx': 'javascript',
|
|
122
|
+
'.md': 'markdown',
|
|
123
|
+
'.markdown': 'markdown',
|
|
124
|
+
'.mjs': 'javascript',
|
|
125
|
+
'.py': 'python',
|
|
126
|
+
'.ts': 'javascript',
|
|
127
|
+
'.tsx': 'javascript',
|
|
128
|
+
'.yaml': 'yaml',
|
|
129
|
+
'.yml': 'yaml'
|
|
130
|
+
};
|
|
112
131
|
|
|
113
132
|
function formatUrlHost(host) {
|
|
114
133
|
if (typeof host !== 'string' || !host) return '127.0.0.1';
|
|
@@ -2537,6 +2556,156 @@ async function execCommandInWebContainer(ctx, containerName, command, options =
|
|
|
2537
2556
|
});
|
|
2538
2557
|
}
|
|
2539
2558
|
|
|
2559
|
+
function buildWebContainerNodeCommand(scriptSource) {
|
|
2560
|
+
return `node <<'__MANYOYO_NODE__'
|
|
2561
|
+
${scriptSource}
|
|
2562
|
+
__MANYOYO_NODE__`;
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2565
|
+
function inferFileLanguage(filePath) {
|
|
2566
|
+
const ext = path.extname(String(filePath || '')).toLowerCase();
|
|
2567
|
+
return FILE_LANGUAGE_MAP[ext] || 'text';
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
async function execJsonCommandInWebContainer(ctx, containerName, command) {
|
|
2571
|
+
const result = await execCommandInWebContainer(ctx, containerName, command);
|
|
2572
|
+
if (result.exitCode !== 0) {
|
|
2573
|
+
throw new Error(result.output || '容器命令执行失败');
|
|
2574
|
+
}
|
|
2575
|
+
try {
|
|
2576
|
+
return JSON.parse(String(result.output || '{}'));
|
|
2577
|
+
} catch (e) {
|
|
2578
|
+
throw new Error('容器返回了无法解析的 JSON');
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
function buildContainerFileListCommand(requestedPath) {
|
|
2583
|
+
return buildWebContainerNodeCommand(`
|
|
2584
|
+
// __MANYOYO_FS_LIST__
|
|
2585
|
+
const fs = require('fs');
|
|
2586
|
+
const path = require('path');
|
|
2587
|
+
|
|
2588
|
+
const requestedPath = ${JSON.stringify(String(requestedPath || '/'))};
|
|
2589
|
+
|
|
2590
|
+
try {
|
|
2591
|
+
const realPath = fs.realpathSync(requestedPath);
|
|
2592
|
+
const stat = fs.statSync(realPath);
|
|
2593
|
+
if (!stat.isDirectory()) {
|
|
2594
|
+
throw new Error('目标不是目录: ' + realPath);
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
const root = path.parse(realPath).root;
|
|
2598
|
+
const parentPath = realPath === root ? '' : path.dirname(realPath);
|
|
2599
|
+
const entries = fs.readdirSync(realPath, { withFileTypes: true })
|
|
2600
|
+
.map(entry => {
|
|
2601
|
+
const fullPath = path.join(realPath, entry.name);
|
|
2602
|
+
let itemStat = null;
|
|
2603
|
+
try {
|
|
2604
|
+
itemStat = fs.lstatSync(fullPath);
|
|
2605
|
+
} catch (e) {
|
|
2606
|
+
itemStat = null;
|
|
2607
|
+
}
|
|
2608
|
+
let kind = 'other';
|
|
2609
|
+
if (entry.isDirectory()) {
|
|
2610
|
+
kind = 'directory';
|
|
2611
|
+
} else if (entry.isFile()) {
|
|
2612
|
+
kind = 'file';
|
|
2613
|
+
} else if (entry.isSymbolicLink()) {
|
|
2614
|
+
kind = 'symlink';
|
|
2615
|
+
}
|
|
2616
|
+
return {
|
|
2617
|
+
name: entry.name,
|
|
2618
|
+
path: fullPath,
|
|
2619
|
+
kind,
|
|
2620
|
+
size: itemStat && typeof itemStat.size === 'number' ? itemStat.size : 0,
|
|
2621
|
+
mtimeMs: itemStat && typeof itemStat.mtimeMs === 'number' ? Math.floor(itemStat.mtimeMs) : 0
|
|
2622
|
+
};
|
|
2623
|
+
})
|
|
2624
|
+
.sort((a, b) => {
|
|
2625
|
+
if (a.kind !== b.kind) {
|
|
2626
|
+
if (a.kind === 'directory') return -1;
|
|
2627
|
+
if (b.kind === 'directory') return 1;
|
|
2628
|
+
}
|
|
2629
|
+
return a.name.localeCompare(b.name, 'zh-CN');
|
|
2630
|
+
});
|
|
2631
|
+
|
|
2632
|
+
process.stdout.write(JSON.stringify({
|
|
2633
|
+
path: realPath,
|
|
2634
|
+
parentPath,
|
|
2635
|
+
entries
|
|
2636
|
+
}));
|
|
2637
|
+
} catch (e) {
|
|
2638
|
+
process.stdout.write(JSON.stringify({
|
|
2639
|
+
error: e && e.message ? e.message : '读取目录失败'
|
|
2640
|
+
}));
|
|
2641
|
+
}
|
|
2642
|
+
`);
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
function buildContainerFileReadCommand(requestedPath) {
|
|
2646
|
+
return buildWebContainerNodeCommand(`
|
|
2647
|
+
// __MANYOYO_FS_READ__
|
|
2648
|
+
const fs = require('fs');
|
|
2649
|
+
|
|
2650
|
+
const requestedPath = ${JSON.stringify(String(requestedPath || ''))};
|
|
2651
|
+
const maxBytes = ${String(WEB_FILE_PREVIEW_MAX_BYTES)};
|
|
2652
|
+
|
|
2653
|
+
function looksBinary(buffer) {
|
|
2654
|
+
const length = Math.min(buffer.length, 4096);
|
|
2655
|
+
let suspicious = 0;
|
|
2656
|
+
for (let i = 0; i < length; i += 1) {
|
|
2657
|
+
const byte = buffer[i];
|
|
2658
|
+
if (byte === 0) {
|
|
2659
|
+
return true;
|
|
2660
|
+
}
|
|
2661
|
+
if (byte < 7 || (byte > 13 && byte < 32)) {
|
|
2662
|
+
suspicious += 1;
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
return length > 0 && (suspicious / length) > 0.12;
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
try {
|
|
2669
|
+
const realPath = fs.realpathSync(requestedPath);
|
|
2670
|
+
const stat = fs.statSync(realPath);
|
|
2671
|
+
if (!stat.isFile()) {
|
|
2672
|
+
throw new Error('目标不是文件: ' + realPath);
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
const size = stat.size;
|
|
2676
|
+
const readBytes = Math.min(size, maxBytes);
|
|
2677
|
+
const buffer = Buffer.alloc(readBytes);
|
|
2678
|
+
const fd = fs.openSync(realPath, 'r');
|
|
2679
|
+
try {
|
|
2680
|
+
fs.readSync(fd, buffer, 0, readBytes, 0);
|
|
2681
|
+
} finally {
|
|
2682
|
+
fs.closeSync(fd);
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
if (looksBinary(buffer)) {
|
|
2686
|
+
process.stdout.write(JSON.stringify({
|
|
2687
|
+
path: realPath,
|
|
2688
|
+
kind: 'binary',
|
|
2689
|
+
size,
|
|
2690
|
+
truncated: size > maxBytes
|
|
2691
|
+
}));
|
|
2692
|
+
} else {
|
|
2693
|
+
process.stdout.write(JSON.stringify({
|
|
2694
|
+
path: realPath,
|
|
2695
|
+
kind: 'text',
|
|
2696
|
+
size,
|
|
2697
|
+
truncated: size > maxBytes,
|
|
2698
|
+
content: buffer.toString('utf8')
|
|
2699
|
+
}));
|
|
2700
|
+
}
|
|
2701
|
+
} catch (e) {
|
|
2702
|
+
process.stdout.write(JSON.stringify({
|
|
2703
|
+
error: e && e.message ? e.message : '读取文件失败'
|
|
2704
|
+
}));
|
|
2705
|
+
}
|
|
2706
|
+
`);
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2540
2709
|
async function execAgentInWebContainerStream(ctx, state, sessionRefOrContainerName, command, options = {}) {
|
|
2541
2710
|
const opts = options && typeof options === 'object' ? options : {};
|
|
2542
2711
|
const sessionRef = typeof sessionRefOrContainerName === 'string'
|
|
@@ -3435,6 +3604,61 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
3435
3604
|
});
|
|
3436
3605
|
}
|
|
3437
3606
|
},
|
|
3607
|
+
{
|
|
3608
|
+
method: 'GET',
|
|
3609
|
+
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/fs\/list$/),
|
|
3610
|
+
handler: async match => {
|
|
3611
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
3612
|
+
if (!sessionRef) {
|
|
3613
|
+
return;
|
|
3614
|
+
}
|
|
3615
|
+
const requestUrl = new URL(req.url || '/api/sessions/x/fs/list', 'http://localhost');
|
|
3616
|
+
const targetPath = String(requestUrl.searchParams.get('path') || '/').trim() || '/';
|
|
3617
|
+
|
|
3618
|
+
await ensureWebContainer(ctx, state, sessionRef.containerName, sessionRef);
|
|
3619
|
+
const payload = await execJsonCommandInWebContainer(
|
|
3620
|
+
ctx,
|
|
3621
|
+
sessionRef.containerName,
|
|
3622
|
+
buildContainerFileListCommand(targetPath)
|
|
3623
|
+
);
|
|
3624
|
+
if (payload && payload.error) {
|
|
3625
|
+
sendJson(res, 400, { error: payload.error });
|
|
3626
|
+
return;
|
|
3627
|
+
}
|
|
3628
|
+
sendJson(res, 200, payload);
|
|
3629
|
+
}
|
|
3630
|
+
},
|
|
3631
|
+
{
|
|
3632
|
+
method: 'GET',
|
|
3633
|
+
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/fs\/read$/),
|
|
3634
|
+
handler: async match => {
|
|
3635
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
3636
|
+
if (!sessionRef) {
|
|
3637
|
+
return;
|
|
3638
|
+
}
|
|
3639
|
+
const requestUrl = new URL(req.url || '/api/sessions/x/fs/read', 'http://localhost');
|
|
3640
|
+
const targetPath = String(requestUrl.searchParams.get('path') || '').trim();
|
|
3641
|
+
if (!targetPath) {
|
|
3642
|
+
sendJson(res, 400, { error: 'path 不能为空' });
|
|
3643
|
+
return;
|
|
3644
|
+
}
|
|
3645
|
+
|
|
3646
|
+
await ensureWebContainer(ctx, state, sessionRef.containerName, sessionRef);
|
|
3647
|
+
const payload = await execJsonCommandInWebContainer(
|
|
3648
|
+
ctx,
|
|
3649
|
+
sessionRef.containerName,
|
|
3650
|
+
buildContainerFileReadCommand(targetPath)
|
|
3651
|
+
);
|
|
3652
|
+
if (payload && payload.error) {
|
|
3653
|
+
sendJson(res, 400, { error: payload.error });
|
|
3654
|
+
return;
|
|
3655
|
+
}
|
|
3656
|
+
if (payload && payload.kind === 'text') {
|
|
3657
|
+
payload.language = inferFileLanguage(payload.path);
|
|
3658
|
+
}
|
|
3659
|
+
sendJson(res, 200, payload);
|
|
3660
|
+
}
|
|
3661
|
+
},
|
|
3438
3662
|
{
|
|
3439
3663
|
method: 'GET',
|
|
3440
3664
|
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/detail$/),
|
|
@@ -3872,7 +4096,12 @@ async function startWebServer(options) {
|
|
|
3872
4096
|
const appFrontendMatch = pathname.match(/^\/app\/frontend\/([A-Za-z0-9._-]+)$/);
|
|
3873
4097
|
if (req.method === 'GET' && appFrontendMatch) {
|
|
3874
4098
|
const assetName = appFrontendMatch[1];
|
|
3875
|
-
if (!(assetName === 'app.css'
|
|
4099
|
+
if (!(assetName === 'app.css'
|
|
4100
|
+
|| assetName === 'app.js'
|
|
4101
|
+
|| assetName === 'markdown.css'
|
|
4102
|
+
|| assetName === 'markdown-renderer.js'
|
|
4103
|
+
|| assetName === 'file-browser.js'
|
|
4104
|
+
|| assetName === 'codemirror.bundle.js')) {
|
|
3876
4105
|
sendHtml(res, 404, '<h1>404 Not Found</h1>');
|
|
3877
4106
|
return;
|
|
3878
4107
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xcanwin/manyoyo",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.9.0",
|
|
4
4
|
"imageVersion": "1.9.0-common",
|
|
5
5
|
"playwrightCliVersion": "0.1.1",
|
|
6
6
|
"description": "AI Agent CLI Security Sandbox for Docker and Podman",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|
|
33
33
|
"dev:release": "node scripts/dev-release.js",
|
|
34
|
+
"build:web-editor": "node scripts/build-web-code-editor.js",
|
|
34
35
|
"install-link": "npm link",
|
|
35
36
|
"test": "jest --coverage",
|
|
36
37
|
"test:unit": "jest test/",
|
|
@@ -66,6 +67,19 @@
|
|
|
66
67
|
"ws": "^8.20.0"
|
|
67
68
|
},
|
|
68
69
|
"devDependencies": {
|
|
70
|
+
"@codemirror/commands": "^6.10.3",
|
|
71
|
+
"@codemirror/lang-css": "^6.3.1",
|
|
72
|
+
"@codemirror/lang-html": "^6.4.11",
|
|
73
|
+
"@codemirror/lang-javascript": "^6.2.5",
|
|
74
|
+
"@codemirror/lang-json": "^6.0.2",
|
|
75
|
+
"@codemirror/lang-markdown": "^6.5.0",
|
|
76
|
+
"@codemirror/lang-python": "^6.2.1",
|
|
77
|
+
"@codemirror/lang-yaml": "^6.1.3",
|
|
78
|
+
"@codemirror/language": "^6.12.3",
|
|
79
|
+
"@codemirror/search": "^6.6.0",
|
|
80
|
+
"@codemirror/state": "^6.6.0",
|
|
81
|
+
"@codemirror/view": "^6.41.0",
|
|
82
|
+
"codemirror": "^6.0.2",
|
|
69
83
|
"jest": "^30.3.0",
|
|
70
84
|
"vitepress": "^1.6.4"
|
|
71
85
|
},
|