@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 +6 -6
- package/bin/manyoyo.js +215 -43
- package/docker/manyoyo.Dockerfile +13 -70
- package/lib/web/server.js +31 -1
- package/{config.example.json → manyoyo.example.json} +35 -25
- package/package.json +3 -3
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.
|
|
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.
|
|
100
|
+
manyoyo build --iv 1.8.4-common
|
|
101
101
|
|
|
102
102
|
# 构建 full 版本
|
|
103
|
-
manyoyo build --iv 1.8.
|
|
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 -
|
|
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
|
|
126
|
+
if (isIp) {
|
|
128
127
|
return value;
|
|
129
128
|
}
|
|
130
129
|
|
|
131
|
-
console.error(`${RED}⚠️ 错误: serve
|
|
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 = '
|
|
146
|
-
let portText =
|
|
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
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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>', '
|
|
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>', '
|
|
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>', '
|
|
855
|
-
.option('-s, --shell <command>', '
|
|
856
|
-
.option('--ss, --shell-suffix <command>', '
|
|
857
|
-
.option('-
|
|
858
|
-
.option('-
|
|
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>', '
|
|
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 模式解析配置 (
|
|
869
|
-
.option('-
|
|
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('-
|
|
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(
|
|
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
|
|
966
|
-
~/.manyoyo/run/c.json
|
|
1114
|
+
~/.manyoyo/manyoyo.json 全局配置文件 (JSON5格式,支持注释)
|
|
1115
|
+
~/.manyoyo/run/c.json 运行配置示例
|
|
967
1116
|
|
|
968
1117
|
路径规则:
|
|
969
|
-
run -r 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 /
|
|
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
|
|
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(
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
318
|
-
|
|
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',
|
|
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
|
-
//
|
|
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.
|
|
9
|
+
"imageVersion": "1.8.4-common",
|
|
10
10
|
"containerMode": "common",
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"
|
|
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
|
-
"
|
|
25
|
-
"serverPass": "change-this-password",
|
|
37
|
+
"quiet": ["tip", "cmd"],
|
|
26
38
|
|
|
27
|
-
//
|
|
28
|
-
"env": {
|
|
29
|
-
"IS_SANDBOX": "1"
|
|
30
|
-
},
|
|
31
|
-
"envFile": [],
|
|
32
|
-
"volumes": [],
|
|
33
|
-
"ports": ["8888:8888"],
|
|
39
|
+
// 构建参数(数组累加)
|
|
34
40
|
"imageBuildArgs": [],
|
|
35
|
-
|
|
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.
|
|
4
|
-
"imageVersion": "1.8.
|
|
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
|
-
"
|
|
53
|
+
"manyoyo.example.json"
|
|
54
54
|
],
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@playwright/mcp": "0.0.68",
|