@xcanwin/manyoyo 5.8.9 → 5.8.10

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.
@@ -1,114 +0,0 @@
1
- 'use strict';
2
-
3
- function createSystemApiRoutes(deps) {
4
- const {
5
- req,
6
- res,
7
- ctx,
8
- state,
9
- fs,
10
- os,
11
- path,
12
- withJsonBody,
13
- sendJson,
14
- expandHomeAliasPath,
15
- readWebConfigSnapshot,
16
- buildSafeWebConfigSnapshot,
17
- restoreWebConfigSecrets,
18
- parseAndValidateConfigRaw,
19
- buildConfigDefaults
20
- } = deps;
21
-
22
- return [
23
- {
24
- method: 'GET',
25
- match: currentPath => currentPath === '/api/fs/directories' ? [] : null,
26
- handler: async () => {
27
- const requestUrl = new URL(req.url || '/api/fs/directories', 'http://localhost');
28
- const requestedPath = expandHomeAliasPath(String(requestUrl.searchParams.get('path') || '').trim() || os.homedir());
29
- const requestedBasePath = expandHomeAliasPath(String(requestUrl.searchParams.get('basePath') || '').trim());
30
- const realPath = fs.realpathSync(requestedPath);
31
- if (!fs.statSync(realPath).isDirectory()) {
32
- sendJson(res, 400, { error: `目录不存在: ${realPath}` });
33
- return;
34
- }
35
-
36
- let realBasePath = '';
37
- if (requestedBasePath) {
38
- realBasePath = fs.realpathSync(requestedBasePath);
39
- if (!fs.statSync(realBasePath).isDirectory()) {
40
- sendJson(res, 400, { error: `basePath 不是目录: ${realBasePath}` });
41
- return;
42
- }
43
- const relativeToBase = path.relative(realBasePath, realPath);
44
- if (relativeToBase.startsWith('..') || path.isAbsolute(relativeToBase)) {
45
- sendJson(res, 400, { error: '目录超出 basePath 范围' });
46
- return;
47
- }
48
- }
49
-
50
- const parentPath = realBasePath
51
- ? (realPath === realBasePath ? '' : path.dirname(realPath))
52
- : (realPath === path.parse(realPath).root ? '' : path.dirname(realPath));
53
- const entries = fs.readdirSync(realPath, { withFileTypes: true })
54
- .filter(entry => entry && entry.isDirectory())
55
- .map(entry => ({
56
- name: entry.name,
57
- path: path.join(realPath, entry.name)
58
- }))
59
- .sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'));
60
-
61
- sendJson(res, 200, {
62
- currentPath: realPath,
63
- basePath: realBasePath || '',
64
- parentPath,
65
- entries
66
- });
67
- }
68
- },
69
- {
70
- method: 'GET',
71
- match: currentPath => currentPath === '/api/config' ? [] : null,
72
- handler: async () => {
73
- const snapshot = readWebConfigSnapshot(state.webConfigPath);
74
- sendJson(res, 200, buildSafeWebConfigSnapshot(snapshot, ctx));
75
- }
76
- },
77
- {
78
- method: 'PUT',
79
- match: currentPath => currentPath === '/api/config' ? [] : null,
80
- handler: withJsonBody(async payload => {
81
- const raw = typeof payload.raw === 'string' ? payload.raw : '';
82
- if (!raw.trim()) {
83
- sendJson(res, 400, { error: '配置内容不能为空' });
84
- return;
85
- }
86
-
87
- const currentSnapshot = readWebConfigSnapshot(state.webConfigPath);
88
- let finalRaw = raw;
89
- let parsed = null;
90
- try {
91
- finalRaw = restoreWebConfigSecrets(raw, currentSnapshot);
92
- parsed = parseAndValidateConfigRaw(finalRaw);
93
- } catch (e) {
94
- sendJson(res, 400, { error: '配置格式错误', detail: e.message || '解析失败' });
95
- return;
96
- }
97
-
98
- const savePath = path.resolve(state.webConfigPath);
99
- fs.mkdirSync(path.dirname(savePath), { recursive: true });
100
- fs.writeFileSync(savePath, finalRaw, 'utf-8');
101
-
102
- sendJson(res, 200, {
103
- saved: true,
104
- path: savePath,
105
- defaults: buildConfigDefaults(ctx, parsed)
106
- });
107
- })
108
- }
109
- ];
110
- }
111
-
112
- module.exports = {
113
- createSystemApiRoutes
114
- };
@@ -1,205 +0,0 @@
1
- 'use strict';
2
-
3
- function createWebTerminalHelpers(options = {}) {
4
- const WebSocket = options.WebSocket;
5
- const spawn = options.spawn;
6
- const forceKillMs = Number.isInteger(options.forceKillMs) ? options.forceKillMs : 2000;
7
- const defaultCols = Number.isInteger(options.defaultCols) ? options.defaultCols : 120;
8
- const defaultRows = Number.isInteger(options.defaultRows) ? options.defaultRows : 36;
9
- const minCols = Number.isInteger(options.minCols) ? options.minCols : 40;
10
- const minRows = Number.isInteger(options.minRows) ? options.minRows : 12;
11
-
12
- function toPositiveInt(value, fallback) {
13
- const parsed = Number.parseInt(value, 10);
14
- if (!Number.isFinite(parsed) || parsed <= 0) {
15
- return fallback;
16
- }
17
- return parsed;
18
- }
19
-
20
- function getUpgradeStatusText(statusCode) {
21
- if (statusCode === 400) return 'Bad Request';
22
- if (statusCode === 401) return 'Unauthorized';
23
- if (statusCode === 404) return 'Not Found';
24
- if (statusCode === 429) return 'Too Many Requests';
25
- if (statusCode === 500) return 'Internal Server Error';
26
- return 'Error';
27
- }
28
-
29
- function sendTerminalEvent(ws, type, payload = {}) {
30
- if (!ws || ws.readyState !== WebSocket.OPEN) {
31
- return;
32
- }
33
- ws.send(JSON.stringify({ type, ...payload }));
34
- }
35
-
36
- function spawnWebTerminalProcess(ctx, containerName, cols, rows) {
37
- const terminalBootstrap = [
38
- 'MANYOYO_WEB_BASHRC="$(mktemp /tmp/manyoyo-web-bashrc.XXXXXX 2>/dev/null || mktemp)"',
39
- 'cat > "$MANYOYO_WEB_BASHRC" <<\'EOF_MANYOYO_RC\'',
40
- 'if [ -f /etc/bash.bashrc ]; then',
41
- ' . /etc/bash.bashrc',
42
- 'fi',
43
- 'if [ -f ~/.bashrc ]; then',
44
- ' . ~/.bashrc',
45
- 'fi',
46
- 'if [ -n "${MANYOYO_TERM_COLS:-}" ] && [ -n "${MANYOYO_TERM_ROWS:-}" ]; then',
47
- ' COLUMNS="$MANYOYO_TERM_COLS"',
48
- ' LINES="$MANYOYO_TERM_ROWS"',
49
- ' export COLUMNS LINES',
50
- ' stty cols "$MANYOYO_TERM_COLS" rows "$MANYOYO_TERM_ROWS" >/dev/null 2>&1 || true',
51
- 'fi',
52
- 'EOF_MANYOYO_RC',
53
- 'chmod 600 "$MANYOYO_WEB_BASHRC" >/dev/null 2>&1 || true',
54
- 'if command -v script >/dev/null 2>&1; then',
55
- ' exec script -qefc "/bin/bash --rcfile $MANYOYO_WEB_BASHRC -i" /dev/null;',
56
- 'fi;',
57
- 'if command -v python3 >/dev/null 2>&1; then',
58
- ' exec python3 -c \'import os, pty; pty.spawn(["/bin/bash","--rcfile",os.environ.get("MANYOYO_WEB_BASHRC","/dev/null"),"-i"])\';',
59
- 'fi;',
60
- 'if command -v python >/dev/null 2>&1; then',
61
- ' exec python -c \'import os, pty; pty.spawn(["/bin/bash","--rcfile",os.environ.get("MANYOYO_WEB_BASHRC","/dev/null"),"-i"])\';',
62
- 'fi;',
63
- 'echo "[manyoyo] 容器内未找到 script/python,终端将降级为非 TTY 模式" >&2;',
64
- 'exec /bin/bash --rcfile "$MANYOYO_WEB_BASHRC" -i'
65
- ].join('\n');
66
-
67
- const termValue = process.env.TERM && process.env.TERM !== 'dumb' ? process.env.TERM : 'xterm-256color';
68
- const colorTermValue = process.env.COLORTERM || 'truecolor';
69
- const dockerExecArgs = [
70
- 'exec',
71
- '-i',
72
- '-e', `TERM=${termValue}`,
73
- '-e', `COLORTERM=${colorTermValue}`,
74
- '-e', `MANYOYO_TERM_COLS=${String(cols)}`,
75
- '-e', `MANYOYO_TERM_ROWS=${String(rows)}`,
76
- containerName,
77
- '/bin/bash',
78
- '-lc',
79
- terminalBootstrap
80
- ];
81
-
82
- return spawn(ctx.dockerCmd, dockerExecArgs, { stdio: ['pipe', 'pipe', 'pipe'] });
83
- }
84
-
85
- return {
86
- normalizeTerminalSize(cols, rows) {
87
- return {
88
- cols: Math.max(minCols, toPositiveInt(cols, defaultCols)),
89
- rows: Math.max(minRows, toPositiveInt(rows, defaultRows))
90
- };
91
- },
92
- sendWebSocketUpgradeError(socket, statusCode, message) {
93
- const body = String(message || getUpgradeStatusText(statusCode));
94
- const reason = getUpgradeStatusText(statusCode);
95
- if (!socket.destroyed) {
96
- socket.write(
97
- `HTTP/1.1 ${statusCode} ${reason}\r\n` +
98
- 'Content-Type: text/plain; charset=utf-8\r\n' +
99
- 'Connection: close\r\n' +
100
- `Content-Length: ${Buffer.byteLength(body, 'utf-8')}\r\n` +
101
- '\r\n' +
102
- body
103
- );
104
- }
105
- socket.destroy();
106
- },
107
- bindTerminalWebSocket(ctx, state, ws, containerName, cols, rows) {
108
- const sessionId = `${containerName}-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
109
- const ptyProcess = spawnWebTerminalProcess(ctx, containerName, cols, rows);
110
- const session = {
111
- id: sessionId,
112
- containerName,
113
- ptyProcess,
114
- closing: false
115
- };
116
-
117
- state.terminalSessions.set(sessionId, session);
118
- sendTerminalEvent(ws, 'status', {
119
- phase: 'ready',
120
- sessionId,
121
- containerName,
122
- cols,
123
- rows
124
- });
125
-
126
- const cleanup = () => {
127
- if (session.closing) {
128
- return;
129
- }
130
- session.closing = true;
131
- state.terminalSessions.delete(sessionId);
132
- if (ptyProcess && !ptyProcess.killed) {
133
- ptyProcess.kill('SIGTERM');
134
- setTimeout(() => {
135
- if (!ptyProcess.killed) {
136
- ptyProcess.kill('SIGKILL');
137
- }
138
- }, forceKillMs);
139
- }
140
- };
141
-
142
- ptyProcess.stdout.on('data', chunk => {
143
- sendTerminalEvent(ws, 'output', { data: chunk.toString('utf-8') });
144
- });
145
-
146
- ptyProcess.stderr.on('data', chunk => {
147
- sendTerminalEvent(ws, 'output', { data: chunk.toString('utf-8') });
148
- });
149
-
150
- ptyProcess.on('error', err => {
151
- sendTerminalEvent(ws, 'error', {
152
- error: err && err.message ? err.message : '终端进程启动失败'
153
- });
154
- });
155
-
156
- ptyProcess.on('close', (code, signal) => {
157
- sendTerminalEvent(ws, 'status', {
158
- phase: 'closed',
159
- code: typeof code === 'number' ? code : null,
160
- signal: signal || null
161
- });
162
- cleanup();
163
- if (ws.readyState === WebSocket.OPEN) {
164
- ws.close();
165
- }
166
- });
167
-
168
- ws.on('message', raw => {
169
- let payload = null;
170
- try {
171
- payload = JSON.parse(raw.toString('utf-8'));
172
- } catch {
173
- payload = {
174
- type: 'input',
175
- data: raw.toString('utf-8')
176
- };
177
- }
178
- if (!payload || typeof payload !== 'object') {
179
- return;
180
- }
181
-
182
- if (payload.type === 'input' && typeof payload.data === 'string' && payload.data.length) {
183
- ptyProcess.stdin.write(payload.data);
184
- return;
185
- }
186
-
187
- if (payload.type === 'resize') {
188
- return;
189
- }
190
-
191
- if (payload.type === 'close') {
192
- ws.close();
193
- }
194
- });
195
-
196
- ws.on('close', cleanup);
197
- ws.on('error', cleanup);
198
- },
199
- cleanupWebRuntimeState() {}
200
- };
201
- }
202
-
203
- module.exports = {
204
- createWebTerminalHelpers
205
- };
@@ -1,94 +0,0 @@
1
- 'use strict';
2
-
3
- function createWebUpgradeHandler(options = {}) {
4
- const formatUrlHost = options.formatUrlHost || (host => host);
5
- const sendWebSocketUpgradeError = options.sendWebSocketUpgradeError || (() => {});
6
- const getWebAuthSession = options.getWebAuthSession || (() => null);
7
- const parseWebSessionKey = options.parseWebSessionKey || (() => ({ containerName: '', agentId: '' }));
8
- const decodeSessionName = options.decodeSessionName || (value => value);
9
- const safeContainerNamePattern = options.safeContainerNamePattern || /^[A-Za-z0-9_.-]+$/;
10
- const normalizeTerminalSize = options.normalizeTerminalSize || ((cols, rows) => ({ cols, rows }));
11
- const ensureWebContainer = options.ensureWebContainer || (async () => {});
12
- const maxTerminalSessions = Number.isInteger(options.maxTerminalSessions) ? options.maxTerminalSessions : 20;
13
-
14
- return function handleWebUpgradeRequest(req, socket, head, wsServer, ctx, state, listenPort) {
15
- const fallbackHost = `${formatUrlHost(ctx.serverHost)}:${ctx.serverPort}`;
16
- let url;
17
- try {
18
- url = new URL(req.url || '/', `http://${req.headers.host || fallbackHost}`);
19
- } catch {
20
- sendWebSocketUpgradeError(socket, 400, 'Invalid URL');
21
- return;
22
- }
23
-
24
- const terminalMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/terminal\/ws$/);
25
- if (!terminalMatch) {
26
- socket.destroy();
27
- return;
28
- }
29
-
30
- const requestOrigin = req.headers.origin;
31
- if (requestOrigin) {
32
- const allowedOrigins = new Set();
33
- const hostHeader = req.headers.host || '';
34
- if (hostHeader) {
35
- allowedOrigins.add(`http://${hostHeader}`);
36
- allowedOrigins.add(`https://${hostHeader}`);
37
- }
38
- if (ctx.serverHost !== '0.0.0.0') {
39
- allowedOrigins.add(`http://${formatUrlHost(ctx.serverHost)}:${listenPort}`);
40
- if (ctx.serverHost === '127.0.0.1') {
41
- allowedOrigins.add(`http://localhost:${listenPort}`);
42
- }
43
- }
44
- if (allowedOrigins.size > 0 && !allowedOrigins.has(requestOrigin)) {
45
- sendWebSocketUpgradeError(socket, 403, 'Forbidden');
46
- return;
47
- }
48
- }
49
-
50
- const authSession = getWebAuthSession(state, req);
51
- if (!authSession) {
52
- sendWebSocketUpgradeError(socket, 401, 'UNAUTHORIZED');
53
- return;
54
- }
55
-
56
- const sessionRef = parseWebSessionKey(decodeSessionName(terminalMatch[1]));
57
- if (!ctx.isValidContainerName(sessionRef.containerName)) {
58
- sendWebSocketUpgradeError(socket, 400, `containerName 非法: ${sessionRef.containerName}`);
59
- return;
60
- }
61
- if (!safeContainerNamePattern.test(sessionRef.agentId)) {
62
- sendWebSocketUpgradeError(socket, 400, `agentId 非法: ${sessionRef.agentId}`);
63
- return;
64
- }
65
-
66
- if (state.terminalSessions.size >= maxTerminalSessions) {
67
- sendWebSocketUpgradeError(socket, 429, 'TERMINAL_LIMIT_REACHED');
68
- return;
69
- }
70
-
71
- const { cols, rows } = normalizeTerminalSize(
72
- url.searchParams.get('cols'),
73
- url.searchParams.get('rows')
74
- );
75
-
76
- ensureWebContainer(ctx, state, sessionRef.containerName)
77
- .then(() => {
78
- wsServer.handleUpgrade(req, socket, head, ws => {
79
- wsServer.emit('connection', ws, req, {
80
- containerName: sessionRef.containerName,
81
- cols,
82
- rows
83
- });
84
- });
85
- })
86
- .catch(error => {
87
- sendWebSocketUpgradeError(socket, 500, error && error.message ? error.message : '终端创建失败');
88
- });
89
- };
90
- }
91
-
92
- module.exports = {
93
- createWebUpgradeHandler
94
- };