@xcanwin/manyoyo 5.3.7 → 5.3.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.
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 0.0.0.0:3000 -U admin -P 123 &>/dev/null & 后台启动并监听全部网卡
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: () => validateHostPath(runtime),
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);
@@ -0,0 +1,9 @@
1
+ {
2
+ "bypassPermissionsModeAccepted": true,
3
+ "hasCompletedOnboarding": true,
4
+ "env": {
5
+ "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",
6
+ "CLAUDE_CODE_HIDE_ACCOUNT_INFO": "1",
7
+ "DISABLE_AUTOUPDATER": "1"
8
+ }
9
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "statusLine": {
3
+ "type": "command",
4
+ "command": "bash /root/.claude/statusline.sh"
5
+ }
6
+ }
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env bash
2
+ # Claude Code status line script
3
+ # Items: used-tokens, context-used, model-with-reasoning, current-dir
4
+
5
+ input=$(cat)
6
+
7
+ total_input=$(echo "$input" | jq -r '.context_window.total_input_tokens // 0')
8
+ total_output=$(echo "$input" | jq -r '.context_window.total_output_tokens // 0')
9
+ used_pct=$(echo "$input" | jq -r '.context_window.used_percentage // empty')
10
+ model_id=$(echo "$input" | jq -r '.model.id // ""')
11
+ model_name=$(echo "$input" | jq -r '.model.display_name // ""')
12
+ cwd=$(echo "$input" | jq -r '.workspace.current_dir // .cwd // ""')
13
+
14
+ # used-tokens (omit when zero)
15
+ used_tokens=$((total_input + total_output))
16
+ if [ "$used_tokens" -ge 1000000 ]; then
17
+ used_tokens_str="$(awk "BEGIN {printf \"%.1fM\", $used_tokens/1000000}") used"
18
+ elif [ "$used_tokens" -ge 1000 ]; then
19
+ used_tokens_str="$(awk "BEGIN {printf \"%.1fk\", $used_tokens/1000}") used"
20
+ elif [ "$used_tokens" -gt 0 ]; then
21
+ used_tokens_str="${used_tokens} used"
22
+ else
23
+ used_tokens_str=""
24
+ fi
25
+
26
+ # context-used (omit when unknown)
27
+ if [ -n "$used_pct" ]; then
28
+ context_used_str="$(printf "%.0f" "$used_pct")% used"
29
+ else
30
+ context_used_str=""
31
+ fi
32
+
33
+ # model-with-reasoning
34
+ if echo "$model_id" | grep -qi "thinking\|extended"; then
35
+ model_str="${model_name}[T]"
36
+ else
37
+ model_str="${model_name}"
38
+ fi
39
+
40
+ # current-dir (absolute path)
41
+ if [ -n "$cwd" ]; then
42
+ current_dir="$cwd"
43
+ else
44
+ current_dir="$PWD"
45
+ fi
46
+
47
+ # Assemble (skip empty parts)
48
+ parts=()
49
+ [ -n "$used_tokens_str" ] && parts+=("$used_tokens_str")
50
+ [ -n "$context_used_str" ] && parts+=("$context_used_str")
51
+ [ -n "$model_str" ] && parts+=("$model_str")
52
+ [ -n "$current_dir" ] && parts+=("$current_dir")
53
+
54
+ result=""
55
+ for part in "${parts[@]}"; do
56
+ [ -z "$result" ] && result="$part" || result="$result · $part"
57
+ done
58
+ printf "%s" "$result"
@@ -0,0 +1,7 @@
1
+ check_for_update_on_startup = false
2
+
3
+ [analytics]
4
+ enabled = false
5
+
6
+ [tui]
7
+ status_line = ["used-tokens", "context-used", "model-with-reasoning", "current-dir", "five-hour-limit", "weekly-limit"]
@@ -0,0 +1,21 @@
1
+ {
2
+ "privacy": {
3
+ "usageStatisticsEnabled": false
4
+ },
5
+ "general": {
6
+ "previewFeatures": true,
7
+ "enableAutoUpdate": false,
8
+ "enableAutoUpdateNotification": false
9
+ },
10
+ "ui": {
11
+ "showLineNumbers": false
12
+ },
13
+ "security": {
14
+ "auth": {
15
+ "selectedType": "oauth-personal"
16
+ }
17
+ },
18
+ "model": {
19
+ "name": "gemini-3-pro-preview"
20
+ }
21
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "$schema": "https://opencode.ai/config.json",
3
+ "autoupdate": false,
4
+ "model": "Custom_Provider/{env:OPENAI_MODEL}",
5
+ "provider": {
6
+ "Custom_Provider": {
7
+ "npm": "@ai-sdk/openai-compatible",
8
+ "options": {
9
+ "baseURL": "{env:OPENAI_BASE_URL}",
10
+ "apiKey": "{env:OPENAI_API_KEY}",
11
+ "headers": {
12
+ "User-Agent": "opencode"
13
+ }
14
+ },
15
+ "models": {
16
+ "{env:OPENAI_MODEL}": {},
17
+ "claude-sonnet-4-5-20250929": {},
18
+ "gpt-5.2": {}
19
+ }
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,3 @@
1
+ [supervisord]
2
+ user=root
3
+ nodaemon=true
@@ -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
- validateWebHostPath(hostPath);
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({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcanwin/manyoyo",
3
- "version": "5.3.7",
3
+ "version": "5.3.10",
4
4
  "imageVersion": "1.8.4-common",
5
5
  "description": "AI Agent CLI Security Sandbox for Docker and Podman",
6
6
  "keywords": [
@@ -50,6 +50,7 @@
50
50
  "README.md",
51
51
  "LICENSE",
52
52
  "docker/manyoyo.Dockerfile",
53
+ "docker/res/**",
53
54
  "manyoyo.example.json"
54
55
  ],
55
56
  "dependencies": {