@xcanwin/manyoyo 5.3.7 → 5.3.9
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 +2 -0
- package/bin/manyoyo.js +73 -90
- package/lib/serve-log.js +116 -0
- package/lib/web/server.js +5 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -125,6 +125,8 @@ manyoyo rm my-dev
|
|
|
125
125
|
# Web 模式
|
|
126
126
|
manyoyo serve 127.0.0.1:3000
|
|
127
127
|
manyoyo serve 127.0.0.1:3000 -U admin -P 123456
|
|
128
|
+
manyoyo serve 127.0.0.1:3000 -U admin -P 123456 -d
|
|
129
|
+
manyoyo serve 127.0.0.1:3000 -d # 未设置密码时会打印本次随机密码
|
|
128
130
|
|
|
129
131
|
# 查看配置与命令拼装
|
|
130
132
|
manyoyo config show
|
package/bin/manyoyo.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const { spawnSync } = require('child_process');
|
|
3
|
+
const { spawn, spawnSync } = require('child_process');
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const os = require('os');
|
|
@@ -16,6 +16,12 @@ const { buildImage } = require('../lib/image-build');
|
|
|
16
16
|
const { resolveAgentResumeArg, buildAgentResumeCommand } = require('../lib/agent-resume');
|
|
17
17
|
const { runPluginCommand } = require('../lib/plugin');
|
|
18
18
|
const { buildManyoyoLogPath } = require('../lib/log-path');
|
|
19
|
+
const {
|
|
20
|
+
sanitizeSensitiveData,
|
|
21
|
+
sanitizeServeLogText,
|
|
22
|
+
formatServeLogValue,
|
|
23
|
+
getServeProcessSnapshot
|
|
24
|
+
} = require('../lib/serve-log');
|
|
19
25
|
const { version: BIN_VERSION, imageVersion: IMAGE_VERSION_DEFAULT } = require('../package.json');
|
|
20
26
|
const IMAGE_VERSION_BASE = String(IMAGE_VERSION_DEFAULT || '1.0.0').split('-')[0];
|
|
21
27
|
const IMAGE_VERSION_HELP_EXAMPLE = IMAGE_VERSION_DEFAULT || `${IMAGE_VERSION_BASE}-common`;
|
|
@@ -190,93 +196,6 @@ function ensureWebServerAuthCredentials() {
|
|
|
190
196
|
}
|
|
191
197
|
}
|
|
192
198
|
|
|
193
|
-
/**
|
|
194
|
-
* 敏感信息脱敏(用于 config show 输出)
|
|
195
|
-
* @param {Object} obj - 配置对象
|
|
196
|
-
* @returns {Object} 脱敏后的配置对象
|
|
197
|
-
*/
|
|
198
|
-
function sanitizeSensitiveData(obj) {
|
|
199
|
-
const sensitiveKeys = ['KEY', 'TOKEN', 'SECRET', 'PASSWORD', 'PASS', 'AUTH', 'CREDENTIAL'];
|
|
200
|
-
|
|
201
|
-
function sanitizeValue(key, value) {
|
|
202
|
-
if (typeof value !== 'string') return value;
|
|
203
|
-
const upperKey = key.toUpperCase();
|
|
204
|
-
if (sensitiveKeys.some(k => upperKey.includes(k))) {
|
|
205
|
-
if (value.length <= 8) return '****';
|
|
206
|
-
return value.slice(0, 4) + '****' + value.slice(-4);
|
|
207
|
-
}
|
|
208
|
-
return value;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function sanitizeArray(arr) {
|
|
212
|
-
return arr.map(item => {
|
|
213
|
-
if (typeof item === 'string' && item.includes('=')) {
|
|
214
|
-
const idx = item.indexOf('=');
|
|
215
|
-
const key = item.slice(0, idx);
|
|
216
|
-
const value = item.slice(idx + 1);
|
|
217
|
-
return `${key}=${sanitizeValue(key, value)}`;
|
|
218
|
-
}
|
|
219
|
-
return item;
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const result = {};
|
|
224
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
225
|
-
if (Array.isArray(value)) {
|
|
226
|
-
result[key] = sanitizeArray(value);
|
|
227
|
-
} else if (typeof value === 'object' && value !== null) {
|
|
228
|
-
result[key] = sanitizeSensitiveData(value);
|
|
229
|
-
} else {
|
|
230
|
-
result[key] = sanitizeValue(key, value);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
return result;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function stripAnsi(text) {
|
|
237
|
-
if (typeof text !== 'string') return '';
|
|
238
|
-
return text.replace(/\x1b\[[0-9;]*m/g, '');
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function sanitizeServeLogText(input) {
|
|
242
|
-
let text = stripAnsi(String(input || ''));
|
|
243
|
-
if (!text) return text;
|
|
244
|
-
|
|
245
|
-
text = text.replace(/(--pass|-P)\s+\S+/gi, '$1 ****');
|
|
246
|
-
text = text.replace(
|
|
247
|
-
/\b(MANYOYO_SERVER_PASS|OPENAI_API_KEY|ANTHROPIC_AUTH_TOKEN|GEMINI_API_KEY|GOOGLE_API_KEY|OPENCODE_API_KEY)\s*=\s*([^\s'"]+)/gi,
|
|
248
|
-
'$1=****'
|
|
249
|
-
);
|
|
250
|
-
text = text.replace(
|
|
251
|
-
/("?(?:password|pass|token|api[_-]?key|authorization|cookie)"?\s*[:=]\s*)("[^"]*"|'[^']*'|[^,\s]+)/gi,
|
|
252
|
-
'$1"****"'
|
|
253
|
-
);
|
|
254
|
-
return text;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function formatServeLogValue(value) {
|
|
258
|
-
if (value instanceof Error) {
|
|
259
|
-
return sanitizeServeLogText(value.stack || value.message || String(value));
|
|
260
|
-
}
|
|
261
|
-
if (typeof value === 'object' && value !== null) {
|
|
262
|
-
try {
|
|
263
|
-
return sanitizeServeLogText(JSON.stringify(sanitizeSensitiveData(value)));
|
|
264
|
-
} catch (e) {
|
|
265
|
-
return sanitizeServeLogText(String(value));
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
return sanitizeServeLogText(String(value));
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function getServeProcessSnapshot() {
|
|
272
|
-
return {
|
|
273
|
-
pid: process.pid,
|
|
274
|
-
ppid: process.ppid,
|
|
275
|
-
cwd: process.cwd(),
|
|
276
|
-
argv: Array.isArray(process.argv) ? process.argv.slice() : []
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
|
|
280
199
|
function createServeLogger() {
|
|
281
200
|
function formatLocalTimestamp(date = new Date()) {
|
|
282
201
|
const y = date.getFullYear();
|
|
@@ -1120,7 +1039,8 @@ async function setupCommander() {
|
|
|
1120
1039
|
${MANYOYO_NAME} run -n test -- -c 恢复之前会话
|
|
1121
1040
|
${MANYOYO_NAME} run -x "echo 123" 使用完整命令
|
|
1122
1041
|
${MANYOYO_NAME} serve 127.0.0.1:3000 启动本机网页服务
|
|
1123
|
-
${MANYOYO_NAME} serve
|
|
1042
|
+
${MANYOYO_NAME} serve 127.0.0.1:3000 -d 后台启动;未设密码时会打印本次随机密码
|
|
1043
|
+
${MANYOYO_NAME} serve 0.0.0.0:3000 -U admin -P 123 -d 后台启动并监听全部网卡
|
|
1124
1044
|
${MANYOYO_NAME} playwright up host-headless 启动 playwright 默认场景(推荐)
|
|
1125
1045
|
${MANYOYO_NAME} plugin playwright up host-headless 通过 plugin 命名空间启动
|
|
1126
1046
|
${MANYOYO_NAME} run -n test -q tip -q cmd 多次使用静默选项
|
|
@@ -1163,6 +1083,7 @@ Notes:
|
|
|
1163
1083
|
|
|
1164
1084
|
const serveCommand = program.command('serve [listen]').description('启动网页交互服务 (默认 127.0.0.1:3000)');
|
|
1165
1085
|
applyRunStyleOptions(serveCommand, { includeRmOnExit: false, includeWebAuthOptions: true });
|
|
1086
|
+
serveCommand.option('-d, --detach', '后台启动网页服务并立即返回');
|
|
1166
1087
|
serveCommand.action((listen, options) => {
|
|
1167
1088
|
selectAction('serve', {
|
|
1168
1089
|
...options,
|
|
@@ -1498,6 +1419,7 @@ Notes:
|
|
|
1498
1419
|
isRemoveMode,
|
|
1499
1420
|
isShowCommandMode,
|
|
1500
1421
|
isServerMode,
|
|
1422
|
+
isServerDetach: Boolean(selectedAction === 'serve' && options.detach),
|
|
1501
1423
|
isPluginMode: false
|
|
1502
1424
|
};
|
|
1503
1425
|
}
|
|
@@ -1524,6 +1446,7 @@ function createRuntimeContext(modeState = {}) {
|
|
|
1524
1446
|
showCommand: Boolean(modeState.isShowCommandMode),
|
|
1525
1447
|
rmOnExit: RM_ON_EXIT,
|
|
1526
1448
|
serverMode: Boolean(modeState.isServerMode),
|
|
1449
|
+
serverDetach: Boolean(modeState.isServerDetach),
|
|
1527
1450
|
serverHost: SERVER_HOST,
|
|
1528
1451
|
serverPort: SERVER_PORT,
|
|
1529
1452
|
serverAuthUser: SERVER_AUTH_USER,
|
|
@@ -1558,6 +1481,62 @@ function validateHostPath(runtime) {
|
|
|
1558
1481
|
}
|
|
1559
1482
|
}
|
|
1560
1483
|
|
|
1484
|
+
function validateHostPathOrThrow(hostPath) {
|
|
1485
|
+
if (!fs.existsSync(hostPath)) {
|
|
1486
|
+
throw new Error(`宿主机路径不存在: ${hostPath}`);
|
|
1487
|
+
}
|
|
1488
|
+
const realHostPath = fs.realpathSync(hostPath);
|
|
1489
|
+
const homeDir = process.env.HOME || '/home';
|
|
1490
|
+
if (realHostPath === '/' || realHostPath === '/home' || realHostPath === homeDir) {
|
|
1491
|
+
throw new Error('不允许挂载根目录或home目录。');
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
function buildDetachedServeArgv(argv) {
|
|
1496
|
+
const result = [];
|
|
1497
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1498
|
+
const arg = String(argv[i] || '');
|
|
1499
|
+
if (arg === '-d' || arg === '--detach') {
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1502
|
+
result.push(arg);
|
|
1503
|
+
}
|
|
1504
|
+
return result;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
function buildDetachedServeEnv(runtime) {
|
|
1508
|
+
const env = { ...process.env };
|
|
1509
|
+
if (runtime.serverAuthUser) {
|
|
1510
|
+
env.MANYOYO_SERVER_USER = runtime.serverAuthUser;
|
|
1511
|
+
}
|
|
1512
|
+
if (runtime.serverAuthPass) {
|
|
1513
|
+
env.MANYOYO_SERVER_PASS = runtime.serverAuthPass;
|
|
1514
|
+
}
|
|
1515
|
+
return env;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
function relaunchServeDetached(runtime) {
|
|
1519
|
+
const serveLog = buildManyoyoLogPath('serve');
|
|
1520
|
+
fs.mkdirSync(serveLog.dir, { recursive: true });
|
|
1521
|
+
|
|
1522
|
+
const child = spawn(process.argv[0], buildDetachedServeArgv(process.argv.slice(1)), {
|
|
1523
|
+
detached: true,
|
|
1524
|
+
stdio: 'ignore',
|
|
1525
|
+
env: buildDetachedServeEnv(runtime)
|
|
1526
|
+
});
|
|
1527
|
+
child.unref();
|
|
1528
|
+
|
|
1529
|
+
console.log(`${GREEN}✅ serve 已转入后台运行${NC}`);
|
|
1530
|
+
console.log(`PID: ${child.pid}`);
|
|
1531
|
+
console.log(`日志: ${serveLog.path}`);
|
|
1532
|
+
console.log(`登录用户名: ${runtime.serverAuthUser}`);
|
|
1533
|
+
if (runtime.serverAuthPassAuto) {
|
|
1534
|
+
console.log(`登录密码(本次随机): ${runtime.serverAuthPass}`);
|
|
1535
|
+
} else {
|
|
1536
|
+
console.log('登录密码: 使用你配置的 serve -P / serverPass / MANYOYO_SERVER_PASS');
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1561
1540
|
/**
|
|
1562
1541
|
* 等待容器就绪(使用指数退避算法)
|
|
1563
1542
|
* @param {string} containerName - 容器名称
|
|
@@ -1850,7 +1829,7 @@ async function runWebServerMode(runtime) {
|
|
|
1850
1829
|
containerEnvs: runtime.containerEnvs,
|
|
1851
1830
|
containerVolumes: runtime.containerVolumes,
|
|
1852
1831
|
containerPorts: runtime.containerPorts,
|
|
1853
|
-
validateHostPath:
|
|
1832
|
+
validateHostPath: value => validateHostPathOrThrow(value),
|
|
1854
1833
|
formatDate,
|
|
1855
1834
|
isValidContainerName,
|
|
1856
1835
|
containerExists,
|
|
@@ -1892,6 +1871,10 @@ async function main() {
|
|
|
1892
1871
|
|
|
1893
1872
|
// 2. Start web server mode
|
|
1894
1873
|
if (runtime.serverMode) {
|
|
1874
|
+
if (runtime.serverDetach) {
|
|
1875
|
+
relaunchServeDetached(runtime);
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1895
1878
|
const serveLogger = createServeLogger();
|
|
1896
1879
|
runtime.logger = serveLogger;
|
|
1897
1880
|
installServeProcessDiagnostics(serveLogger);
|
package/lib/serve-log.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
function stripAnsi(text) {
|
|
2
|
+
if (typeof text !== 'string') return '';
|
|
3
|
+
return text.replace(/\x1b\[[0-9;]*m/g, '');
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function sanitizeProcessArgv(argv) {
|
|
7
|
+
if (!Array.isArray(argv)) {
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const result = [];
|
|
12
|
+
for (let i = 0; i < argv.length; i++) {
|
|
13
|
+
const arg = String(argv[i] || '');
|
|
14
|
+
if (arg === '--pass' || arg === '-P') {
|
|
15
|
+
result.push(arg);
|
|
16
|
+
if (i + 1 < argv.length) {
|
|
17
|
+
result.push('****');
|
|
18
|
+
i += 1;
|
|
19
|
+
}
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (arg.startsWith('--pass=')) {
|
|
23
|
+
result.push('--pass=****');
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
result.push(arg);
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function sanitizeServeLogText(input) {
|
|
32
|
+
let text = stripAnsi(String(input || ''));
|
|
33
|
+
if (!text) return text;
|
|
34
|
+
|
|
35
|
+
text = text.replace(/(--pass|-P)\s+\S+/gi, '$1 ****');
|
|
36
|
+
text = text.replace(/("--pass"|"-P")\s*,\s*"[^"]*"/gi, '$1,"****"');
|
|
37
|
+
text = text.replace(/--pass=([^\s'"]+)/gi, '--pass=****');
|
|
38
|
+
text = text.replace(
|
|
39
|
+
/\b(MANYOYO_SERVER_PASS|OPENAI_API_KEY|ANTHROPIC_AUTH_TOKEN|GEMINI_API_KEY|GOOGLE_API_KEY|OPENCODE_API_KEY)\s*=\s*([^\s'"]+)/gi,
|
|
40
|
+
'$1=****'
|
|
41
|
+
);
|
|
42
|
+
text = text.replace(
|
|
43
|
+
/(?<![-\w])("?(?:password|pass|token|api[_-]?key|authorization|cookie)"?\s*[:=]\s*)("[^"]*"|'[^']*'|[^,\s]+)/gi,
|
|
44
|
+
'$1"****"'
|
|
45
|
+
);
|
|
46
|
+
return text;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function sanitizeSensitiveData(obj) {
|
|
50
|
+
const sensitiveKeys = ['KEY', 'TOKEN', 'SECRET', 'PASSWORD', 'PASS', 'AUTH', 'CREDENTIAL'];
|
|
51
|
+
|
|
52
|
+
function sanitizeValue(key, value) {
|
|
53
|
+
if (typeof value !== 'string') return value;
|
|
54
|
+
const upperKey = key.toUpperCase();
|
|
55
|
+
if (sensitiveKeys.some(k => upperKey.includes(k))) {
|
|
56
|
+
if (value.length <= 8) return '****';
|
|
57
|
+
return value.slice(0, 4) + '****' + value.slice(-4);
|
|
58
|
+
}
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function sanitizeArray(arr) {
|
|
63
|
+
return arr.map(item => {
|
|
64
|
+
if (typeof item === 'string' && item.includes('=')) {
|
|
65
|
+
const idx = item.indexOf('=');
|
|
66
|
+
const key = item.slice(0, idx);
|
|
67
|
+
const value = item.slice(idx + 1);
|
|
68
|
+
return `${key}=${sanitizeValue(key, value)}`;
|
|
69
|
+
}
|
|
70
|
+
return item;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const result = {};
|
|
75
|
+
for (const [key, value] of Object.entries(obj || {})) {
|
|
76
|
+
if (Array.isArray(value)) {
|
|
77
|
+
result[key] = sanitizeArray(value);
|
|
78
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
79
|
+
result[key] = sanitizeSensitiveData(value);
|
|
80
|
+
} else {
|
|
81
|
+
result[key] = sanitizeValue(key, value);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function formatServeLogValue(value) {
|
|
88
|
+
if (value instanceof Error) {
|
|
89
|
+
return sanitizeServeLogText(value.stack || value.message || String(value));
|
|
90
|
+
}
|
|
91
|
+
if (typeof value === 'object' && value !== null) {
|
|
92
|
+
try {
|
|
93
|
+
return sanitizeServeLogText(JSON.stringify(sanitizeSensitiveData(value)));
|
|
94
|
+
} catch (e) {
|
|
95
|
+
return sanitizeServeLogText(String(value));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return sanitizeServeLogText(String(value));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getServeProcessSnapshot(processRef = process) {
|
|
102
|
+
return {
|
|
103
|
+
pid: processRef.pid,
|
|
104
|
+
ppid: processRef.ppid,
|
|
105
|
+
cwd: typeof processRef.cwd === 'function' ? processRef.cwd() : '',
|
|
106
|
+
argv: sanitizeProcessArgv(Array.isArray(processRef.argv) ? processRef.argv.slice() : [])
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = {
|
|
111
|
+
sanitizeProcessArgv,
|
|
112
|
+
sanitizeServeLogText,
|
|
113
|
+
sanitizeSensitiveData,
|
|
114
|
+
formatServeLogValue,
|
|
115
|
+
getServeProcessSnapshot
|
|
116
|
+
};
|
package/lib/web/server.js
CHANGED
|
@@ -812,7 +812,11 @@ function buildCreateRuntime(ctx, state, payload) {
|
|
|
812
812
|
validateContainerNameStrict(containerName);
|
|
813
813
|
|
|
814
814
|
const hostPath = pickFirstString(requestOptions.hostPath, config.hostPath, ctx.hostPath);
|
|
815
|
-
|
|
815
|
+
if (typeof ctx.validateHostPath === 'function') {
|
|
816
|
+
ctx.validateHostPath(hostPath);
|
|
817
|
+
} else {
|
|
818
|
+
validateWebHostPath(hostPath);
|
|
819
|
+
}
|
|
816
820
|
|
|
817
821
|
const containerPath = pickFirstString(requestOptions.containerPath, config.containerPath, ctx.containerPath, hostPath) || hostPath;
|
|
818
822
|
const imageName = pickFirstString(requestOptions.imageName, config.imageName, ctx.imageName);
|
|
@@ -1798,7 +1802,6 @@ async function startWebServer(options) {
|
|
|
1798
1802
|
terminalSessions: new Map()
|
|
1799
1803
|
};
|
|
1800
1804
|
|
|
1801
|
-
ctx.validateHostPath();
|
|
1802
1805
|
ensureWebHistoryDir(state.webHistoryDir);
|
|
1803
1806
|
|
|
1804
1807
|
const wsServer = new WebSocket.Server({
|