@xcanwin/manyoyo 5.2.23 → 5.3.5

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
@@ -46,7 +46,7 @@
46
46
  ```bash
47
47
  npm install -g @xcanwin/manyoyo # 安装
48
48
  podman pull ubuntu:24.04 # 仅 Podman 需要
49
- manyoyo build --iv 1.8.0-common # 构建镜像
49
+ manyoyo build --iv 1.8.4-common # 构建镜像
50
50
  manyoyo init all # 从本机 Agent 配置迁移到 ~/.manyoyo
51
51
  manyoyo run -r claude # 使用 manyoyo.json 的 runs.claude 启动
52
52
  ```
@@ -97,10 +97,10 @@ npm install -g @xcanwin/manyoyo
97
97
 
98
98
  ```bash
99
99
  # 构建 common 版本(推荐)
100
- manyoyo build --iv 1.8.0-common
100
+ manyoyo build --iv 1.8.4-common
101
101
 
102
102
  # 构建 full 版本
103
- manyoyo build --iv 1.8.0-full
103
+ manyoyo build --iv 1.8.4-full
104
104
 
105
105
  # 构建自定义版本
106
106
  manyoyo build --iba TOOL=go,codex,java,gemini
@@ -126,8 +126,8 @@ manyoyo ps
126
126
  manyoyo images
127
127
  manyoyo run -n my-dev -x /bin/bash
128
128
  manyoyo rm my-dev
129
- manyoyo serve 3000
130
- manyoyo serve 3000 -u admin -P 123456
129
+ manyoyo serve 127.0.0.1:3000
130
+ manyoyo serve 127.0.0.1:3000 -U admin -P 123456
131
131
 
132
132
  # 调试配置与命令拼装
133
133
  manyoyo config show
@@ -136,7 +136,7 @@ manyoyo config command
136
136
 
137
137
  ## 配置
138
138
 
139
- 配置优先级:命令行参数 > runs.<name> > 全局配置 > 默认值
139
+ 配置优先级:命令行参数 > runs.<name> > 全局配置 > 默认值
140
140
  详细说明请参考:
141
141
  - [配置系统概览](https://xcanwin.github.io/manyoyo/zh/configuration/)
142
142
  - [环境变量详解](https://xcanwin.github.io/manyoyo/zh/configuration/environment)
package/bin/manyoyo.js CHANGED
@@ -122,13 +122,12 @@ function mergeArrayConfig(globalValue, runValue, cliValue) {
122
122
  function validateServerHost(host, rawServer) {
123
123
  const value = String(host || '').trim();
124
124
  const isIp = net.isIP(value) !== 0;
125
- const isHostName = /^[A-Za-z0-9.-]+$/.test(value);
126
125
 
127
- if (isIp || isHostName) {
126
+ if (isIp) {
128
127
  return value;
129
128
  }
130
129
 
131
- console.error(`${RED}⚠️ 错误: serve 地址格式应为 端口 或 host:port (例如 3000 / 0.0.0.0:3000): ${rawServer}${NC}`);
130
+ console.error(`${RED}⚠️ 错误: serve 地址格式必须为 <ip:port> (例如 127.0.0.1:3000 / 0.0.0.0:3000): ${rawServer}${NC}`);
132
131
  process.exit(1);
133
132
  }
134
133
 
@@ -142,8 +141,8 @@ function parseServerListen(rawServer) {
142
141
  return { host: '127.0.0.1', port: 3000 };
143
142
  }
144
143
 
145
- let host = '127.0.0.1';
146
- let portText = value;
144
+ let host = '';
145
+ let portText = '';
147
146
 
148
147
  const ipv6Match = value.match(/^\[([^\]]+)\]:(\d+)$/);
149
148
  if (ipv6Match) {
@@ -151,12 +150,14 @@ function parseServerListen(rawServer) {
151
150
  portText = ipv6Match[2].trim();
152
151
  } else {
153
152
  const lastColonIndex = value.lastIndexOf(':');
154
- if (lastColonIndex > 0) {
155
- const maybePort = value.slice(lastColonIndex + 1).trim();
156
- if (/^\d+$/.test(maybePort)) {
157
- host = value.slice(0, lastColonIndex).trim();
158
- portText = maybePort;
159
- }
153
+ if (lastColonIndex <= 0) {
154
+ console.error(`${RED}⚠️ 错误: serve 地址格式必须为 <ip:port> (例如 127.0.0.1:3000 / 0.0.0.0:3000): ${rawServer}${NC}`);
155
+ process.exit(1);
156
+ }
157
+ const maybePort = value.slice(lastColonIndex + 1).trim();
158
+ if (/^\d+$/.test(maybePort)) {
159
+ host = value.slice(0, lastColonIndex).trim();
160
+ portText = maybePort;
160
161
  }
161
162
  }
162
163
 
@@ -231,6 +232,149 @@ function sanitizeSensitiveData(obj) {
231
232
  return result;
232
233
  }
233
234
 
235
+ function stripAnsi(text) {
236
+ if (typeof text !== 'string') return '';
237
+ return text.replace(/\x1b\[[0-9;]*m/g, '');
238
+ }
239
+
240
+ function sanitizeServeLogText(input) {
241
+ let text = stripAnsi(String(input || ''));
242
+ if (!text) return text;
243
+
244
+ text = text.replace(/(--pass|-P)\s+\S+/gi, '$1 ****');
245
+ text = text.replace(
246
+ /\b(MANYOYO_SERVER_PASS|OPENAI_API_KEY|ANTHROPIC_AUTH_TOKEN|GEMINI_API_KEY|GOOGLE_API_KEY|OPENCODE_API_KEY)\s*=\s*([^\s'"]+)/gi,
247
+ '$1=****'
248
+ );
249
+ text = text.replace(
250
+ /("?(?:password|pass|token|api[_-]?key|authorization|cookie)"?\s*[:=]\s*)("[^"]*"|'[^']*'|[^,\s]+)/gi,
251
+ '$1"****"'
252
+ );
253
+ return text;
254
+ }
255
+
256
+ function formatServeLogValue(value) {
257
+ if (value instanceof Error) {
258
+ return sanitizeServeLogText(value.stack || value.message || String(value));
259
+ }
260
+ if (typeof value === 'object' && value !== null) {
261
+ try {
262
+ return sanitizeServeLogText(JSON.stringify(sanitizeSensitiveData(value)));
263
+ } catch (e) {
264
+ return sanitizeServeLogText(String(value));
265
+ }
266
+ }
267
+ return sanitizeServeLogText(String(value));
268
+ }
269
+
270
+ function getServeProcessSnapshot() {
271
+ return {
272
+ pid: process.pid,
273
+ ppid: process.ppid,
274
+ cwd: process.cwd(),
275
+ argv: Array.isArray(process.argv) ? process.argv.slice() : []
276
+ };
277
+ }
278
+
279
+ function createServeLogger() {
280
+ function pad2(n) {
281
+ return String(n).padStart(2, '0');
282
+ }
283
+
284
+ function getLocalDateTag(date = new Date()) {
285
+ const y = date.getFullYear();
286
+ const m = pad2(date.getMonth() + 1);
287
+ const d = pad2(date.getDate());
288
+ return `${y}-${m}-${d}`;
289
+ }
290
+
291
+ function formatLocalTimestamp(date = new Date()) {
292
+ const y = date.getFullYear();
293
+ const m = pad2(date.getMonth() + 1);
294
+ const d = pad2(date.getDate());
295
+ const hh = pad2(date.getHours());
296
+ const mm = pad2(date.getMinutes());
297
+ const ss = pad2(date.getSeconds());
298
+ const ms = String(date.getMilliseconds()).padStart(3, '0');
299
+ const offsetMinutes = -date.getTimezoneOffset();
300
+ const sign = offsetMinutes >= 0 ? '+' : '-';
301
+ const abs = Math.abs(offsetMinutes);
302
+ const offH = pad2(Math.floor(abs / 60));
303
+ const offM = pad2(abs % 60);
304
+ return `${y}-${m}-${d}T${hh}:${mm}:${ss}.${ms}${sign}${offH}:${offM}`;
305
+ }
306
+
307
+ const logDir = path.join(os.homedir(), '.manyoyo', 'logs');
308
+ fs.mkdirSync(logDir, { recursive: true });
309
+ const dateTag = getLocalDateTag();
310
+ const logPath = path.join(logDir, `serve-${dateTag}.log`);
311
+
312
+ function write(level, message, extra) {
313
+ const ts = formatLocalTimestamp();
314
+ const parts = [
315
+ `[${ts}]`,
316
+ `[pid:${process.pid}]`,
317
+ `[${String(level || 'INFO').toUpperCase()}]`,
318
+ formatServeLogValue(message)
319
+ ];
320
+ if (extra !== undefined) {
321
+ parts.push(formatServeLogValue(extra));
322
+ }
323
+ fs.appendFileSync(logPath, `${parts.join(' ')}\n`);
324
+ }
325
+
326
+ return {
327
+ path: logPath,
328
+ info: (message, extra) => write('INFO', message, extra),
329
+ warn: (message, extra) => write('WARN', message, extra),
330
+ error: (message, extra) => write('ERROR', message, extra)
331
+ };
332
+ }
333
+
334
+ function installServeProcessDiagnostics(logger) {
335
+ if (!logger || typeof logger.info !== 'function') return;
336
+ if (global.__manyoyoServeDiagInstalled) return;
337
+ global.__manyoyoServeDiagInstalled = true;
338
+
339
+ const signalExitCode = {
340
+ SIGINT: 130,
341
+ SIGTERM: 143,
342
+ SIGHUP: 129
343
+ };
344
+
345
+ process.on('uncaughtException', err => {
346
+ logger.error('uncaughtException', {
347
+ error: err,
348
+ process: getServeProcessSnapshot()
349
+ });
350
+ process.exit(1);
351
+ });
352
+
353
+ process.on('unhandledRejection', reason => {
354
+ logger.error('unhandledRejection', {
355
+ reason,
356
+ process: getServeProcessSnapshot()
357
+ });
358
+ process.exit(1);
359
+ });
360
+
361
+ ['SIGINT', 'SIGTERM', 'SIGHUP'].forEach(signal => {
362
+ process.on(signal, () => {
363
+ logger.warn(`received ${signal}, process will exit`, {
364
+ signal,
365
+ process: getServeProcessSnapshot()
366
+ });
367
+ process.exit(signalExitCode[signal] || 1);
368
+ });
369
+ });
370
+
371
+ process.on('exit', code => {
372
+ logger.info(`process exit with code=${code}`, {
373
+ process: getServeProcessSnapshot()
374
+ });
375
+ });
376
+ }
377
+
234
378
  /**
235
379
  * @typedef {Object} Config
236
380
  * @property {string} [containerName] - 容器名称
@@ -838,41 +982,46 @@ function applyRunStyleOptions(command, options = {}) {
838
982
 
839
983
  command
840
984
  .option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)')
841
- .option('--hp, --host-path <path>', '设置宿主机工作目录 (默认当前路径)')
985
+ .option('--hp, --host-path <path>', '设置宿主机工作目录 (默认: 当前路径)')
842
986
  .option('-n, --cont-name <name>', '设置容器名称')
843
987
  .option('--cp, --cont-path <path>', '设置容器工作目录')
844
- .option('-m, --cont-mode <mode>', '设置容器嵌套容器模式 (common, dind, sock)')
988
+ .option('-m, --cont-mode <mode>', '设置容器嵌套模式 (common, dind, sock; 注意: sock 模式可访问宿主机 Docker socket,风险较高)')
845
989
  .option('--in, --image-name <name>', '指定镜像名称')
846
990
  .option('--iv, --image-ver <version>', '指定镜像版本 (格式: x.y.z-后缀,如 1.7.4-common)');
847
991
 
848
992
  appendArrayOption(command, '-e, --env <env>', '设置环境变量 XXX=YYY (可多次使用)');
849
- appendArrayOption(command, '--ef, --env-file <file>', '设置环境变量通过文件 (仅支持绝对路径,如 /abs/path.env)');
993
+ appendArrayOption(command, '--ef, --env-file <file>', '从环境文件加载变量 (仅支持绝对路径,如 /abs/path.env; 相对路径会报错)');
850
994
  appendArrayOption(command, '-v, --volume <volume>', '绑定挂载卷 XXX:YYY (可多次使用)');
851
995
  appendArrayOption(command, '-p, --port <port>', '设置端口映射 XXX:YYY (可多次使用)');
852
996
 
853
997
  command
854
- .option('--sp, --shell-prefix <command>', '临时环境变量 (作为-s前缀)')
855
- .option('-s, --shell <command>', '指定命令执行')
856
- .option('--ss, --shell-suffix <command>', '指定命令后缀 (追加到-s之后,等价于 -- <args>)')
857
- .option('-x, --shell-full <command...>', '指定完整命令执行 (代替--sp和-s和--命令)')
858
- .option('-y, --yolo <cli>', '使AGENT无需确认 (claude/c, gemini/gm, codex/cx, opencode/oc)');
998
+ .option('--sp, --shell-prefix <command>', '主命令前缀 (常用于临时环境变量)')
999
+ .option('-s, --shell <command>', '主命令')
1000
+ .option('--ss, --shell-suffix <command>', '主命令后缀 (追加到 -s 之后,等价于 -- <args>)')
1001
+ .option('--first-shell-prefix <command>', '首次预执行命令前缀 (仅新建容器生效; 容器已存在时忽略)')
1002
+ .option('--first-shell <command>', '首次预执行命令 (仅新建容器生效; 容器已存在时忽略)')
1003
+ .option('--first-shell-suffix <command>', '首次预执行命令后缀 (仅新建容器生效; 容器已存在时忽略)')
1004
+ .option('-x, --shell-full <command...>', '完整命令 (与 --sp/-s/--ss/-- 互斥)')
1005
+ .option('-y, --yolo <cli>', '使 AGENT 无需确认 (claude(c), gemini(gm), codex(cx), opencode(oc))');
1006
+ appendArrayOption(command, '--first-env <env>', '首次预执行环境变量 XXX=YYY (可多次使用)');
1007
+ appendArrayOption(command, '--first-env-file <file>', '首次预执行环境变量文件 (仅支持绝对路径,如 /abs/path.env)');
859
1008
 
860
1009
  if (includeRmOnExit) {
861
1010
  command.option('--rm-on-exit', '退出后自动删除容器 (一次性模式)');
862
1011
  }
863
1012
 
864
- appendArrayOption(command, '-q, --quiet <item>', '静默显示 (可多次使用: cnew,crm,tip,cmd,full)');
1013
+ appendArrayOption(command, '-q, --quiet <item>', '静默输出 (可多次使用: cnew, crm, tip, cmd, full)');
865
1014
 
866
1015
  if (includeServePreview) {
867
1016
  command
868
- .option('--serve [listen]', '按 serve 模式解析配置 (支持 port 或 host:port)')
869
- .option('-u, --user <username>', '网页服务登录用户名 (默认 admin)')
1017
+ .option('--serve [listen]', '按 serve 模式解析配置 (仅支持 <ip:port>)')
1018
+ .option('-U, --user <username>', '网页服务登录用户名 (默认 admin)')
870
1019
  .option('-P, --pass <password>', '网页服务登录密码 (默认自动生成随机密码)');
871
1020
  }
872
1021
 
873
1022
  if (includeWebAuthOptions) {
874
1023
  command
875
- .option('-u, --user <username>', '网页服务登录用户名 (默认 admin)')
1024
+ .option('-U, --user <username>', '网页服务登录用户名 (默认 admin)')
876
1025
  .option('-P, --pass <password>', '网页服务登录密码 (默认自动生成随机密码)');
877
1026
  }
878
1027
 
@@ -917,7 +1066,7 @@ async function setupCommander() {
917
1066
  const actions = ['up', 'down', 'status', 'health', 'logs'];
918
1067
  actions.forEach(action => {
919
1068
  const sceneCommand = command.command(`${action} [scene]`)
920
- .description(`${action} playwright 场景,scene 默认 host-headless`)
1069
+ .description(`执行 playwright ${action} 场景(scene 默认 host-headless)`)
921
1070
  .option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)');
922
1071
 
923
1072
  if (action === 'up') {
@@ -962,14 +1111,14 @@ async function setupCommander() {
962
1111
  .description('MANYOYO - AI Agent CLI Sandbox\nhttps://github.com/xcanwin/manyoyo')
963
1112
  .addHelpText('after', `
964
1113
  配置文件:
965
- ~/.manyoyo/manyoyo.json 全局配置文件 (JSON5格式,支持注释)
966
- ~/.manyoyo/run/c.json 运行配置示例
1114
+ ~/.manyoyo/manyoyo.json 全局配置文件 (JSON5格式,支持注释)
1115
+ ~/.manyoyo/run/c.json 运行配置示例
967
1116
 
968
1117
  路径规则:
969
- run -r name → ~/.manyoyo/manyoyo.json 的 runs.name
970
- run --ef /abs/path.env → 绝对路径环境文件
971
- run --ss "<args>" → 显式设置命令后缀
972
- run -- <args...> → 直接透传命令后缀(优先级最高)
1118
+ run -r name → ~/.manyoyo/manyoyo.json 的 runs.name
1119
+ run --ef /abs/path.env → 绝对路径环境文件
1120
+ run --ss "<args>" → 显式设置命令后缀
1121
+ run -- <args...> → 直接透传命令后缀(优先级最高)
973
1122
 
974
1123
  示例:
975
1124
  ${MANYOYO_NAME} update 更新 MANYOYO 到最新版本
@@ -977,17 +1126,26 @@ async function setupCommander() {
977
1126
  ${MANYOYO_NAME} init all 从本机 Agent 配置初始化 ~/.manyoyo
978
1127
  ${MANYOYO_NAME} run -r claude 使用 manyoyo.json 的 runs.claude 快速启动
979
1128
  ${MANYOYO_NAME} run -r codex --ss "resume --last" 使用命令后缀
980
- ${MANYOYO_NAME} run -n test --ef /abs/path/myenv.env -y c 使用绝对路径环境变量文件
1129
+ ${MANYOYO_NAME} run -n test --ef /path/ab.env -y c 使用绝对路径环境变量文件
981
1130
  ${MANYOYO_NAME} run -n test -- -c 恢复之前会话
982
- ${MANYOYO_NAME} run -x "echo 123" 指定命令执行
983
- ${MANYOYO_NAME} serve 3000 -u admin -P 123456 启动带登录认证的网页服务
984
- ${MANYOYO_NAME} serve 0.0.0.0:3000 监听全部网卡,便于局域网访问
1131
+ ${MANYOYO_NAME} run -x "echo 123" 使用完整命令
1132
+ ${MANYOYO_NAME} serve 127.0.0.1:3000 启动本机网页服务
1133
+ ${MANYOYO_NAME} serve 0.0.0.0:3000 -U admin -P 123 &>/dev/null & 后台启动并监听全部网卡
985
1134
  ${MANYOYO_NAME} playwright up host-headless 启动 playwright 默认场景(推荐)
986
1135
  ${MANYOYO_NAME} plugin playwright up host-headless 通过 plugin 命名空间启动
987
1136
  ${MANYOYO_NAME} run -n test -q tip -q cmd 多次使用静默选项
988
1137
  `);
989
1138
 
990
- const runCommand = program.command('run').description('启动或连接容器并执行命令');
1139
+ const runCommand = program.command('run').description('启动(容器不存在时)或连接(容器已存在时)容器并执行命令');
1140
+ runCommand.addHelpText('after', `
1141
+ Examples:
1142
+ ${MANYOYO_NAME} run -r codex
1143
+ ${MANYOYO_NAME} run --rm-on-exit -x /bin/bash -lc "node -v"
1144
+ ${MANYOYO_NAME} run -n demo --first-shell "npm ci" -s "npm test"
1145
+
1146
+ Notes:
1147
+ 参数优先级与合并规则(标量覆盖、数组追加、env 按 key 合并)请用 ${MANYOYO_NAME} config show --help 或查看文档。
1148
+ `);
991
1149
  applyRunStyleOptions(runCommand);
992
1150
  runCommand.action(options => selectAction('run', options));
993
1151
 
@@ -1069,7 +1227,7 @@ async function setupCommander() {
1069
1227
  .action(() => selectAction('update', { update: true }));
1070
1228
 
1071
1229
  program.command('install <name>')
1072
- .description(`安装${MANYOYO_NAME}命令 (docker-cli-plugin)`)
1230
+ .description(`安装 ${MANYOYO_NAME} 命令 (docker-cli-plugin)`)
1073
1231
  .action(name => selectAction('install', { install: name }));
1074
1232
 
1075
1233
  program.command('prune')
@@ -1184,15 +1342,15 @@ async function setupCommander() {
1184
1342
  if (mergedShellSuffix) {
1185
1343
  EXEC_COMMAND_SUFFIX = normalizeCommandSuffix(mergedShellSuffix);
1186
1344
  }
1187
- const mergedFirstShellPrefix = pickConfigValue(runFirstConfig.shellPrefix, globalFirstConfig.shellPrefix);
1345
+ const mergedFirstShellPrefix = pickConfigValue(options.firstShellPrefix, runFirstConfig.shellPrefix, globalFirstConfig.shellPrefix);
1188
1346
  if (mergedFirstShellPrefix) {
1189
1347
  FIRST_EXEC_COMMAND_PREFIX = `${mergedFirstShellPrefix} `;
1190
1348
  }
1191
- const mergedFirstShell = pickConfigValue(runFirstConfig.shell, globalFirstConfig.shell);
1349
+ const mergedFirstShell = pickConfigValue(options.firstShell, runFirstConfig.shell, globalFirstConfig.shell);
1192
1350
  if (mergedFirstShell) {
1193
1351
  FIRST_EXEC_COMMAND = mergedFirstShell;
1194
1352
  }
1195
- const mergedFirstShellSuffix = pickConfigValue(runFirstConfig.shellSuffix, globalFirstConfig.shellSuffix);
1353
+ const mergedFirstShellSuffix = pickConfigValue(options.firstShellSuffix, runFirstConfig.shellSuffix, globalFirstConfig.shellSuffix);
1196
1354
  if (mergedFirstShellSuffix) {
1197
1355
  FIRST_EXEC_COMMAND_SUFFIX = normalizeCommandSuffix(mergedFirstShellSuffix);
1198
1356
  }
@@ -1221,13 +1379,15 @@ async function setupCommander() {
1221
1379
 
1222
1380
  const firstEnvFileList = [
1223
1381
  ...toArray(globalFirstConfig.envFile),
1224
- ...toArray(runFirstConfig.envFile)
1382
+ ...toArray(runFirstConfig.envFile),
1383
+ ...(options.firstEnvFile || [])
1225
1384
  ].filter(Boolean);
1226
1385
  firstEnvFileList.forEach(ef => addEnvFileTo(FIRST_CONTAINER_ENVS, ef));
1227
1386
 
1228
1387
  const firstEnvMap = {
1229
1388
  ...normalizeJsonEnvMap(globalFirstConfig.env, '全局配置 first'),
1230
- ...normalizeJsonEnvMap(runFirstConfig.env, '运行配置 first')
1389
+ ...normalizeJsonEnvMap(runFirstConfig.env, '运行配置 first'),
1390
+ ...normalizeCliEnvMap(options.firstEnv)
1231
1391
  };
1232
1392
  Object.entries(firstEnvMap).forEach(([key, value]) => addEnvTo(FIRST_CONTAINER_ENVS, `${key}=${value}`));
1233
1393
 
@@ -1378,7 +1538,8 @@ function createRuntimeContext(modeState = {}) {
1378
1538
  serverPort: SERVER_PORT,
1379
1539
  serverAuthUser: SERVER_AUTH_USER,
1380
1540
  serverAuthPass: SERVER_AUTH_PASS,
1381
- serverAuthPassAuto: SERVER_AUTH_PASS_AUTO
1541
+ serverAuthPassAuto: SERVER_AUTH_PASS_AUTO,
1542
+ logger: null
1382
1543
  };
1383
1544
  }
1384
1545
 
@@ -1716,7 +1877,8 @@ async function runWebServerMode(runtime) {
1716
1877
  BLUE,
1717
1878
  CYAN,
1718
1879
  NC
1719
- }
1880
+ },
1881
+ logger: runtime.logger
1720
1882
  });
1721
1883
  }
1722
1884
 
@@ -1740,6 +1902,16 @@ async function main() {
1740
1902
 
1741
1903
  // 2. Start web server mode
1742
1904
  if (runtime.serverMode) {
1905
+ const serveLogger = createServeLogger();
1906
+ runtime.logger = serveLogger;
1907
+ installServeProcessDiagnostics(serveLogger);
1908
+ serveLogger.info('serve startup requested', {
1909
+ host: runtime.serverHost,
1910
+ port: runtime.serverPort,
1911
+ user: runtime.serverAuthUser || 'admin(auto/default)',
1912
+ process: getServeProcessSnapshot()
1913
+ });
1914
+ console.log(`${CYAN}📝 serve 日志文件: ${YELLOW}${serveLogger.path}${NC}`);
1743
1915
  await runWebServerMode(runtime);
1744
1916
  return;
1745
1917
  }
@@ -131,6 +131,7 @@ EOX
131
131
 
132
132
  # 从 cache-stage 复制 Node.js(缓存或下载)
133
133
  COPY --from=cache-stage /opt/node /usr/local
134
+ COPY ./docker/res/ /tmp/docker-res/
134
135
  ARG GIT_SSL_NO_VERIFY=false
135
136
 
136
137
  RUN <<EOX
@@ -145,17 +146,10 @@ RUN <<EOX
145
146
  # 安装 Claude CLI
146
147
  npm install -g @anthropic-ai/claude-code
147
148
  mkdir -p ~/.claude/plugins/marketplaces/
148
- cat > ~/.claude.json <<EOF
149
- {
150
- "bypassPermissionsModeAccepted": true,
151
- "hasCompletedOnboarding": true,
152
- "env": {
153
- "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",
154
- "CLAUDE_CODE_HIDE_ACCOUNT_INFO": "1",
155
- "DISABLE_AUTOUPDATER": "1"
156
- }
157
- }
158
- EOF
149
+ cp /tmp/docker-res/claude/claude.json ~/.claude.json
150
+ cp /tmp/docker-res/claude/settings.json ~/.claude/settings.json
151
+ cp /tmp/docker-res/claude/statusline.sh ~/.claude/statusline.sh
152
+ chmod +x ~/.claude/statusline.sh
159
153
  claude plugin marketplace add https://github.com/anthropics/claude-plugins-official
160
154
  claude plugin install ralph-loop@claude-plugins-official
161
155
  claude plugin install typescript-lsp@claude-plugins-official
@@ -172,12 +166,7 @@ EOF
172
166
  # 安装 Codex CLI
173
167
  npm install -g @openai/codex
174
168
  mkdir -p ~/.codex
175
- cat > ~/.codex/config.toml <<EOF
176
- check_for_update_on_startup = false
177
-
178
- [analytics]
179
- enabled = false
180
- EOF
169
+ cp /tmp/docker-res/codex/config.toml ~/.codex/config.toml
181
170
  mkdir -p "$HOME/.codex/skills"
182
171
  git clone --depth 1 https://github.com/openai/skills.git /tmp/openai-skills
183
172
  cp -a /tmp/openai-skills/skills/.system "$HOME/.codex/skills/.system"
@@ -204,59 +193,14 @@ EOF
204
193
  npm install -g @google/gemini-cli
205
194
  mkdir -p ~/.gemini/ ~/.gemini/tmp/bin
206
195
  ln -s $(which rg) ~/.gemini/tmp/bin/rg
207
- cat > ~/.gemini/settings.json <<EOF
208
- {
209
- "privacy": {
210
- "usageStatisticsEnabled": false
211
- },
212
- "general": {
213
- "previewFeatures": true,
214
- "enableAutoUpdate": false,
215
- "enableAutoUpdateNotification": false
216
- },
217
- "ui": {
218
- "showLineNumbers": false
219
- },
220
- "security": {
221
- "auth": {
222
- "selectedType": "oauth-personal"
223
- }
224
- },
225
- "model": {
226
- "name": "gemini-3-pro-preview"
227
- }
228
- }
229
- EOF
196
+ cp /tmp/docker-res/gemini/settings.json ~/.gemini/settings.json
230
197
  ;; esac
231
198
 
232
199
  # 安装 OpenCode CLI
233
200
  case ",$TOOL," in *,full,*|*,opencode,*)
234
201
  npm install -g opencode-ai
235
202
  mkdir -p ~/.config/opencode/
236
- cat > ~/.config/opencode/opencode.json <<EOF
237
- {
238
- "\$schema": "https://opencode.ai/config.json",
239
- "autoupdate": false,
240
- "model": "Custom_Provider/{env:OPENAI_MODEL}",
241
- "provider": {
242
- "Custom_Provider": {
243
- "npm": "@ai-sdk/openai-compatible",
244
- "options": {
245
- "baseURL": "{env:OPENAI_BASE_URL}",
246
- "apiKey": "{env:OPENAI_API_KEY}",
247
- "headers": {
248
- "User-Agent": "opencode"
249
- }
250
- },
251
- "models": {
252
- "{env:OPENAI_MODEL}": {},
253
- "claude-sonnet-4-5-20250929": {},
254
- "gpt-5.2": {}
255
- }
256
- }
257
- }
258
- }
259
- EOF
203
+ cp /tmp/docker-res/opencode/opencode.json ~/.config/opencode/opencode.json
260
204
  ;; esac
261
205
 
262
206
  # 清理
@@ -313,13 +257,12 @@ RUN <<EOX
313
257
  rm -rf /tmp/gopls-cache
314
258
  EOX
315
259
 
260
+ # 配置 supervisor
261
+ COPY ./docker/res/supervisor/s.conf /etc/supervisor/conf.d/s.conf
262
+
316
263
  RUN <<EOX
317
- # 配置 supervisor
318
- cat > /etc/supervisor/conf.d/s.conf << EOF
319
- [supervisord]
320
- user=root
321
- nodaemon=true
322
- EOF
264
+ # 清理
265
+ rm -rf /tmp/* /var/tmp/* /var/log/apt /var/log/*.log /var/lib/apt/lists/* ~/.cache ~/.npm ~/go/pkg/mod/cache
323
266
  EOX
324
267
 
325
268
  WORKDIR /tmp
package/lib/web/server.js CHANGED
@@ -1746,6 +1746,11 @@ async function handleWebApi(req, res, pathname, ctx, state) {
1746
1746
  }
1747
1747
 
1748
1748
  async function startWebServer(options) {
1749
+ const fallbackLogger = {
1750
+ info: () => {},
1751
+ warn: () => {},
1752
+ error: () => {}
1753
+ };
1749
1754
  const ctx = {
1750
1755
  serverHost: options.serverHost || '127.0.0.1',
1751
1756
  serverPort: options.serverPort,
@@ -1773,6 +1778,7 @@ async function startWebServer(options) {
1773
1778
  dockerExecArgs: options.dockerExecArgs,
1774
1779
  showImagePullHint: options.showImagePullHint,
1775
1780
  removeContainer: options.removeContainer,
1781
+ logger: options.logger && typeof options.logger.info === 'function' ? options.logger : fallbackLogger,
1776
1782
  colors: options.colors || {
1777
1783
  GREEN: '',
1778
1784
  CYAN: '',
@@ -1799,6 +1805,9 @@ async function startWebServer(options) {
1799
1805
  noServer: true,
1800
1806
  maxPayload: 1024 * 1024
1801
1807
  });
1808
+ wsServer.on('error', err => {
1809
+ ctx.logger.error('ws server error', err);
1810
+ });
1802
1811
 
1803
1812
  wsServer.on('connection', (ws, req, meta = {}) => {
1804
1813
  const containerName = meta.containerName;
@@ -1869,6 +1878,11 @@ async function startWebServer(options) {
1869
1878
 
1870
1879
  sendHtml(res, 404, '<h1>404 Not Found</h1>');
1871
1880
  } catch (e) {
1881
+ ctx.logger.error('http request error', {
1882
+ method: req && req.method ? req.method : '',
1883
+ url: req && req.url ? req.url : '',
1884
+ message: e && e.message ? e.message : 'Server Error'
1885
+ });
1872
1886
  if ((req.url || '').startsWith('/api/')) {
1873
1887
  sendJson(res, 500, { error: e.message || 'Server Error' });
1874
1888
  } else {
@@ -1876,6 +1890,12 @@ async function startWebServer(options) {
1876
1890
  }
1877
1891
  }
1878
1892
  });
1893
+ server.on('error', err => {
1894
+ ctx.logger.error('http server error', err);
1895
+ });
1896
+ server.on('close', () => {
1897
+ ctx.logger.warn('http server closed');
1898
+ });
1879
1899
 
1880
1900
  server.on('upgrade', (req, socket, head) => {
1881
1901
  const fallbackHost = `${formatUrlHost(ctx.serverHost)}:${ctx.serverPort}`;
@@ -1956,7 +1976,10 @@ async function startWebServer(options) {
1956
1976
  let listenPort = ctx.serverPort;
1957
1977
 
1958
1978
  await new Promise((resolve, reject) => {
1959
- server.once('error', reject);
1979
+ server.once('error', err => {
1980
+ ctx.logger.error('http server listen failed', err);
1981
+ reject(err);
1982
+ });
1960
1983
  server.listen(ctx.serverPort, ctx.serverHost, () => {
1961
1984
  const address = server.address();
1962
1985
  if (address && typeof address === 'object' && typeof address.port === 'number') {
@@ -1975,6 +1998,12 @@ async function startWebServer(options) {
1975
1998
  } else {
1976
1999
  console.log(`${CYAN}🔐 登录密码: 使用你配置的 serve -P / serverPass / MANYOYO_SERVER_PASS${NC}`);
1977
2000
  }
2001
+ ctx.logger.info('web server started', {
2002
+ host: ctx.serverHost,
2003
+ port: listenPort,
2004
+ authUser: ctx.authUser,
2005
+ authPassAuto: Boolean(ctx.authPassAuto)
2006
+ });
1978
2007
  resolve();
1979
2008
  });
1980
2009
  });
@@ -1985,6 +2014,7 @@ async function startWebServer(options) {
1985
2014
  host: ctx.serverHost,
1986
2015
  port: listenPort,
1987
2016
  close: () => new Promise(resolve => {
2017
+ ctx.logger.info('web server closing');
1988
2018
  for (const session of state.terminalSessions.values()) {
1989
2019
  const ptyProcess = session && session.ptyProcess;
1990
2020
  if (ptyProcess && !ptyProcess.killed) {
@@ -1,38 +1,48 @@
1
1
  {
2
2
  // MANYOYO 全局配置文件,保存到 ~/.manyoyo/manyoyo.json
3
3
 
4
- // 覆盖型参数(命令行 > runs.<name> > 全局配置)
4
+ // 容器基础参数(覆盖型:命令行 > runs.<name> > 全局配置 > 默认值)
5
5
  "containerName": "my-dev",
6
6
  "hostPath": "/path/to/your/project",
7
7
  "containerPath": "/path/to/your/project",
8
8
  "imageName": "localhost/xcanwin/manyoyo",
9
- "imageVersion": "1.8.0-common",
9
+ "imageVersion": "1.8.4-common",
10
10
  "containerMode": "common",
11
- "shellPrefix": "",
12
- "shell": "",
13
- "shellSuffix": "",
14
- "agentPromptCommand": "",
11
+
12
+ // 容器启动环境(env 按 key 合并覆盖;其余数组参数会累加)
13
+ // 仅支持绝对路径,如 /abs/path.env
14
+ "envFile": [],
15
+ "env": {
16
+ "IS_SANDBOX": "1"
17
+ },
18
+ "volumes": [],
19
+ "ports": ["8888:8888"],
20
+
15
21
  // 仅首次创建容器时执行一次(创建后、常规 shell 前)
16
22
  "first": {
23
+ // 仅支持绝对路径,如 /abs/path.env
24
+ "envFile": [],
25
+ "env": {},
17
26
  "shellPrefix": "",
18
27
  "shell": "",
19
- "shellSuffix": "",
20
- "env": {},
21
- "envFile": []
28
+ "shellSuffix": ""
22
29
  },
30
+
31
+ // 常规命令参数(覆盖型:命令行 > runs.<name> > 全局配置 > 默认值)
32
+ "shellPrefix": "",
33
+ "shell": "",
34
+ "shellSuffix": "",
35
+ "agentPromptCommand": "",
23
36
  "yolo": "",
24
- "serverUser": "admin",
25
- "serverPass": "change-this-password",
37
+ "quiet": ["tip", "cmd"],
26
38
 
27
- // 合并型参数(env 按 key 合并覆盖;其余数组参数会累加)
28
- "env": {
29
- "IS_SANDBOX": "1"
30
- },
31
- "envFile": [],
32
- "volumes": [],
33
- "ports": ["8888:8888"],
39
+ // 构建参数(数组累加)
34
40
  "imageBuildArgs": [],
35
- "quiet": ["tip", "cmd"],
41
+
42
+ // 网页认证参数(覆盖型;支持环境变量兜底)
43
+ // 优先级:命令行 > runs.<name> > 全局配置 > 环境变量 > 默认值
44
+ "serverUser": "admin",
45
+ "serverPass": "change-this-password",
36
46
 
37
47
  // 可选插件(manyoyo playwright / manyoyo plugin playwright)
38
48
  "plugins": {
@@ -65,15 +75,15 @@
65
75
  "runs": {
66
76
  "claude": {
67
77
  "containerName": "my-claude-{now}",
68
- "yolo": "c",
69
- "shell": "claude",
70
- "agentPromptCommand": "claude -p {prompt}",
71
- "first": {
72
- "shell": "echo first-init"
73
- },
74
78
  "env": {
75
79
  "ANTHROPIC_MODEL": "claude-sonnet-4-5"
76
80
  },
81
+ "first": {
82
+ "shell": "echo first-init"
83
+ },
84
+ "shell": "claude",
85
+ "agentPromptCommand": "claude -p {prompt}",
86
+ "yolo": "c",
77
87
  "plugins": {
78
88
  "playwright": {
79
89
  "runtime": "container"
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xcanwin/manyoyo",
3
- "version": "5.2.23",
4
- "imageVersion": "1.8.1-common",
3
+ "version": "5.3.5",
4
+ "imageVersion": "1.8.4-common",
5
5
  "description": "AI Agent CLI Security Sandbox for Docker and Podman",
6
6
  "keywords": [
7
7
  "manyoyo",
@@ -50,7 +50,7 @@
50
50
  "README.md",
51
51
  "LICENSE",
52
52
  "docker/manyoyo.Dockerfile",
53
- "config.example.json"
53
+ "manyoyo.example.json"
54
54
  ],
55
55
  "dependencies": {
56
56
  "@playwright/mcp": "0.0.68",