@xcanwin/manyoyo 5.3.1 → 5.3.6
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 +205 -43
- package/lib/log-path.js +41 -0
- package/lib/web/server.js +31 -1
- package/{config.example.json → manyoyo.example.json} +35 -25
- package/package.json +2 -2
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
|
@@ -15,6 +15,7 @@ const { initAgentConfigs } = require('../lib/init-config');
|
|
|
15
15
|
const { buildImage } = require('../lib/image-build');
|
|
16
16
|
const { resolveAgentResumeArg, buildAgentResumeCommand } = require('../lib/agent-resume');
|
|
17
17
|
const { runPluginCommand } = require('../lib/plugin');
|
|
18
|
+
const { buildManyoyoLogPath } = require('../lib/log-path');
|
|
18
19
|
const { version: BIN_VERSION, imageVersion: IMAGE_VERSION_DEFAULT } = require('../package.json');
|
|
19
20
|
const IMAGE_VERSION_BASE = String(IMAGE_VERSION_DEFAULT || '1.0.0').split('-')[0];
|
|
20
21
|
const IMAGE_VERSION_HELP_EXAMPLE = IMAGE_VERSION_DEFAULT || `${IMAGE_VERSION_BASE}-common`;
|
|
@@ -122,13 +123,12 @@ function mergeArrayConfig(globalValue, runValue, cliValue) {
|
|
|
122
123
|
function validateServerHost(host, rawServer) {
|
|
123
124
|
const value = String(host || '').trim();
|
|
124
125
|
const isIp = net.isIP(value) !== 0;
|
|
125
|
-
const isHostName = /^[A-Za-z0-9.-]+$/.test(value);
|
|
126
126
|
|
|
127
|
-
if (isIp
|
|
127
|
+
if (isIp) {
|
|
128
128
|
return value;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
console.error(`${RED}⚠️ 错误: serve
|
|
131
|
+
console.error(`${RED}⚠️ 错误: serve 地址格式必须为 <ip:port> (例如 127.0.0.1:3000 / 0.0.0.0:3000): ${rawServer}${NC}`);
|
|
132
132
|
process.exit(1);
|
|
133
133
|
}
|
|
134
134
|
|
|
@@ -142,8 +142,8 @@ function parseServerListen(rawServer) {
|
|
|
142
142
|
return { host: '127.0.0.1', port: 3000 };
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
let host = '
|
|
146
|
-
let portText =
|
|
145
|
+
let host = '';
|
|
146
|
+
let portText = '';
|
|
147
147
|
|
|
148
148
|
const ipv6Match = value.match(/^\[([^\]]+)\]:(\d+)$/);
|
|
149
149
|
if (ipv6Match) {
|
|
@@ -151,12 +151,14 @@ function parseServerListen(rawServer) {
|
|
|
151
151
|
portText = ipv6Match[2].trim();
|
|
152
152
|
} else {
|
|
153
153
|
const lastColonIndex = value.lastIndexOf(':');
|
|
154
|
-
if (lastColonIndex
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
154
|
+
if (lastColonIndex <= 0) {
|
|
155
|
+
console.error(`${RED}⚠️ 错误: serve 地址格式必须为 <ip:port> (例如 127.0.0.1:3000 / 0.0.0.0:3000): ${rawServer}${NC}`);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
const maybePort = value.slice(lastColonIndex + 1).trim();
|
|
159
|
+
if (/^\d+$/.test(maybePort)) {
|
|
160
|
+
host = value.slice(0, lastColonIndex).trim();
|
|
161
|
+
portText = maybePort;
|
|
160
162
|
}
|
|
161
163
|
}
|
|
162
164
|
|
|
@@ -231,6 +233,138 @@ function sanitizeSensitiveData(obj) {
|
|
|
231
233
|
return result;
|
|
232
234
|
}
|
|
233
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
|
+
function createServeLogger() {
|
|
281
|
+
function formatLocalTimestamp(date = new Date()) {
|
|
282
|
+
const y = date.getFullYear();
|
|
283
|
+
const m = String(date.getMonth() + 1).padStart(2, '0');
|
|
284
|
+
const d = String(date.getDate()).padStart(2, '0');
|
|
285
|
+
const hh = String(date.getHours()).padStart(2, '0');
|
|
286
|
+
const mm = String(date.getMinutes()).padStart(2, '0');
|
|
287
|
+
const ss = String(date.getSeconds()).padStart(2, '0');
|
|
288
|
+
const ms = String(date.getMilliseconds()).padStart(3, '0');
|
|
289
|
+
const offsetMinutes = -date.getTimezoneOffset();
|
|
290
|
+
const sign = offsetMinutes >= 0 ? '+' : '-';
|
|
291
|
+
const abs = Math.abs(offsetMinutes);
|
|
292
|
+
const offH = String(Math.floor(abs / 60)).padStart(2, '0');
|
|
293
|
+
const offM = String(abs % 60).padStart(2, '0');
|
|
294
|
+
return `${y}-${m}-${d}T${hh}:${mm}:${ss}.${ms}${sign}${offH}:${offM}`;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const serveLog = buildManyoyoLogPath('serve');
|
|
298
|
+
const logDir = serveLog.dir;
|
|
299
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
300
|
+
const logPath = serveLog.path;
|
|
301
|
+
|
|
302
|
+
function write(level, message, extra) {
|
|
303
|
+
const ts = formatLocalTimestamp();
|
|
304
|
+
const parts = [
|
|
305
|
+
`[${ts}]`,
|
|
306
|
+
`[pid:${process.pid}]`,
|
|
307
|
+
`[${String(level || 'INFO').toUpperCase()}]`,
|
|
308
|
+
formatServeLogValue(message)
|
|
309
|
+
];
|
|
310
|
+
if (extra !== undefined) {
|
|
311
|
+
parts.push(formatServeLogValue(extra));
|
|
312
|
+
}
|
|
313
|
+
fs.appendFileSync(logPath, `${parts.join(' ')}\n`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
path: logPath,
|
|
318
|
+
info: (message, extra) => write('INFO', message, extra),
|
|
319
|
+
warn: (message, extra) => write('WARN', message, extra),
|
|
320
|
+
error: (message, extra) => write('ERROR', message, extra)
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function installServeProcessDiagnostics(logger) {
|
|
325
|
+
if (!logger || typeof logger.info !== 'function') return;
|
|
326
|
+
if (global.__manyoyoServeDiagInstalled) return;
|
|
327
|
+
global.__manyoyoServeDiagInstalled = true;
|
|
328
|
+
|
|
329
|
+
const signalExitCode = {
|
|
330
|
+
SIGINT: 130,
|
|
331
|
+
SIGTERM: 143,
|
|
332
|
+
SIGHUP: 129
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
process.on('uncaughtException', err => {
|
|
336
|
+
logger.error('uncaughtException', {
|
|
337
|
+
error: err,
|
|
338
|
+
process: getServeProcessSnapshot()
|
|
339
|
+
});
|
|
340
|
+
process.exit(1);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
process.on('unhandledRejection', reason => {
|
|
344
|
+
logger.error('unhandledRejection', {
|
|
345
|
+
reason,
|
|
346
|
+
process: getServeProcessSnapshot()
|
|
347
|
+
});
|
|
348
|
+
process.exit(1);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
['SIGINT', 'SIGTERM', 'SIGHUP'].forEach(signal => {
|
|
352
|
+
process.on(signal, () => {
|
|
353
|
+
logger.warn(`received ${signal}, process will exit`, {
|
|
354
|
+
signal,
|
|
355
|
+
process: getServeProcessSnapshot()
|
|
356
|
+
});
|
|
357
|
+
process.exit(signalExitCode[signal] || 1);
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
process.on('exit', code => {
|
|
362
|
+
logger.info(`process exit with code=${code}`, {
|
|
363
|
+
process: getServeProcessSnapshot()
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
234
368
|
/**
|
|
235
369
|
* @typedef {Object} Config
|
|
236
370
|
* @property {string} [containerName] - 容器名称
|
|
@@ -838,41 +972,46 @@ function applyRunStyleOptions(command, options = {}) {
|
|
|
838
972
|
|
|
839
973
|
command
|
|
840
974
|
.option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)')
|
|
841
|
-
.option('--hp, --host-path <path>', '设置宿主机工作目录 (
|
|
975
|
+
.option('--hp, --host-path <path>', '设置宿主机工作目录 (默认: 当前路径)')
|
|
842
976
|
.option('-n, --cont-name <name>', '设置容器名称')
|
|
843
977
|
.option('--cp, --cont-path <path>', '设置容器工作目录')
|
|
844
|
-
.option('-m, --cont-mode <mode>', '
|
|
978
|
+
.option('-m, --cont-mode <mode>', '设置容器嵌套模式 (common, dind, sock; 注意: sock 模式可访问宿主机 Docker socket,风险较高)')
|
|
845
979
|
.option('--in, --image-name <name>', '指定镜像名称')
|
|
846
980
|
.option('--iv, --image-ver <version>', '指定镜像版本 (格式: x.y.z-后缀,如 1.7.4-common)');
|
|
847
981
|
|
|
848
982
|
appendArrayOption(command, '-e, --env <env>', '设置环境变量 XXX=YYY (可多次使用)');
|
|
849
|
-
appendArrayOption(command, '--ef, --env-file <file>', '
|
|
983
|
+
appendArrayOption(command, '--ef, --env-file <file>', '从环境文件加载变量 (仅支持绝对路径,如 /abs/path.env; 相对路径会报错)');
|
|
850
984
|
appendArrayOption(command, '-v, --volume <volume>', '绑定挂载卷 XXX:YYY (可多次使用)');
|
|
851
985
|
appendArrayOption(command, '-p, --port <port>', '设置端口映射 XXX:YYY (可多次使用)');
|
|
852
986
|
|
|
853
987
|
command
|
|
854
|
-
.option('--sp, --shell-prefix <command>', '
|
|
855
|
-
.option('-s, --shell <command>', '
|
|
856
|
-
.option('--ss, --shell-suffix <command>', '
|
|
857
|
-
.option('-
|
|
858
|
-
.option('-
|
|
988
|
+
.option('--sp, --shell-prefix <command>', '主命令前缀 (常用于临时环境变量)')
|
|
989
|
+
.option('-s, --shell <command>', '主命令')
|
|
990
|
+
.option('--ss, --shell-suffix <command>', '主命令后缀 (追加到 -s 之后,等价于 -- <args>)')
|
|
991
|
+
.option('--first-shell-prefix <command>', '首次预执行命令前缀 (仅新建容器生效; 容器已存在时忽略)')
|
|
992
|
+
.option('--first-shell <command>', '首次预执行命令 (仅新建容器生效; 容器已存在时忽略)')
|
|
993
|
+
.option('--first-shell-suffix <command>', '首次预执行命令后缀 (仅新建容器生效; 容器已存在时忽略)')
|
|
994
|
+
.option('-x, --shell-full <command...>', '完整命令 (与 --sp/-s/--ss/-- 互斥)')
|
|
995
|
+
.option('-y, --yolo <cli>', '使 AGENT 无需确认 (claude(c), gemini(gm), codex(cx), opencode(oc))');
|
|
996
|
+
appendArrayOption(command, '--first-env <env>', '首次预执行环境变量 XXX=YYY (可多次使用)');
|
|
997
|
+
appendArrayOption(command, '--first-env-file <file>', '首次预执行环境变量文件 (仅支持绝对路径,如 /abs/path.env)');
|
|
859
998
|
|
|
860
999
|
if (includeRmOnExit) {
|
|
861
1000
|
command.option('--rm-on-exit', '退出后自动删除容器 (一次性模式)');
|
|
862
1001
|
}
|
|
863
1002
|
|
|
864
|
-
appendArrayOption(command, '-q, --quiet <item>', '
|
|
1003
|
+
appendArrayOption(command, '-q, --quiet <item>', '静默输出 (可多次使用: cnew, crm, tip, cmd, full)');
|
|
865
1004
|
|
|
866
1005
|
if (includeServePreview) {
|
|
867
1006
|
command
|
|
868
|
-
.option('--serve [listen]', '按 serve 模式解析配置 (
|
|
869
|
-
.option('-
|
|
1007
|
+
.option('--serve [listen]', '按 serve 模式解析配置 (仅支持 <ip:port>)')
|
|
1008
|
+
.option('-U, --user <username>', '网页服务登录用户名 (默认 admin)')
|
|
870
1009
|
.option('-P, --pass <password>', '网页服务登录密码 (默认自动生成随机密码)');
|
|
871
1010
|
}
|
|
872
1011
|
|
|
873
1012
|
if (includeWebAuthOptions) {
|
|
874
1013
|
command
|
|
875
|
-
.option('-
|
|
1014
|
+
.option('-U, --user <username>', '网页服务登录用户名 (默认 admin)')
|
|
876
1015
|
.option('-P, --pass <password>', '网页服务登录密码 (默认自动生成随机密码)');
|
|
877
1016
|
}
|
|
878
1017
|
|
|
@@ -917,7 +1056,7 @@ async function setupCommander() {
|
|
|
917
1056
|
const actions = ['up', 'down', 'status', 'health', 'logs'];
|
|
918
1057
|
actions.forEach(action => {
|
|
919
1058
|
const sceneCommand = command.command(`${action} [scene]`)
|
|
920
|
-
.description(
|
|
1059
|
+
.description(`执行 playwright ${action} 场景(scene 默认 host-headless)`)
|
|
921
1060
|
.option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)');
|
|
922
1061
|
|
|
923
1062
|
if (action === 'up') {
|
|
@@ -962,14 +1101,14 @@ async function setupCommander() {
|
|
|
962
1101
|
.description('MANYOYO - AI Agent CLI Sandbox\nhttps://github.com/xcanwin/manyoyo')
|
|
963
1102
|
.addHelpText('after', `
|
|
964
1103
|
配置文件:
|
|
965
|
-
~/.manyoyo/manyoyo.json
|
|
966
|
-
~/.manyoyo/run/c.json
|
|
1104
|
+
~/.manyoyo/manyoyo.json 全局配置文件 (JSON5格式,支持注释)
|
|
1105
|
+
~/.manyoyo/run/c.json 运行配置示例
|
|
967
1106
|
|
|
968
1107
|
路径规则:
|
|
969
|
-
run -r name
|
|
970
|
-
run --ef /abs/path.env
|
|
971
|
-
run --ss "<args>"
|
|
972
|
-
run -- <args...>
|
|
1108
|
+
run -r name → ~/.manyoyo/manyoyo.json 的 runs.name
|
|
1109
|
+
run --ef /abs/path.env → 绝对路径环境文件
|
|
1110
|
+
run --ss "<args>" → 显式设置命令后缀
|
|
1111
|
+
run -- <args...> → 直接透传命令后缀(优先级最高)
|
|
973
1112
|
|
|
974
1113
|
示例:
|
|
975
1114
|
${MANYOYO_NAME} update 更新 MANYOYO 到最新版本
|
|
@@ -977,17 +1116,26 @@ async function setupCommander() {
|
|
|
977
1116
|
${MANYOYO_NAME} init all 从本机 Agent 配置初始化 ~/.manyoyo
|
|
978
1117
|
${MANYOYO_NAME} run -r claude 使用 manyoyo.json 的 runs.claude 快速启动
|
|
979
1118
|
${MANYOYO_NAME} run -r codex --ss "resume --last" 使用命令后缀
|
|
980
|
-
${MANYOYO_NAME} run -n test --ef /
|
|
1119
|
+
${MANYOYO_NAME} run -n test --ef /path/ab.env -y c 使用绝对路径环境变量文件
|
|
981
1120
|
${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
|
|
1121
|
+
${MANYOYO_NAME} run -x "echo 123" 使用完整命令
|
|
1122
|
+
${MANYOYO_NAME} serve 127.0.0.1:3000 启动本机网页服务
|
|
1123
|
+
${MANYOYO_NAME} serve 0.0.0.0:3000 -U admin -P 123 &>/dev/null & 后台启动并监听全部网卡
|
|
985
1124
|
${MANYOYO_NAME} playwright up host-headless 启动 playwright 默认场景(推荐)
|
|
986
1125
|
${MANYOYO_NAME} plugin playwright up host-headless 通过 plugin 命名空间启动
|
|
987
1126
|
${MANYOYO_NAME} run -n test -q tip -q cmd 多次使用静默选项
|
|
988
1127
|
`);
|
|
989
1128
|
|
|
990
|
-
const runCommand = program.command('run').description('
|
|
1129
|
+
const runCommand = program.command('run').description('启动(容器不存在时)或连接(容器已存在时)容器并执行命令');
|
|
1130
|
+
runCommand.addHelpText('after', `
|
|
1131
|
+
Examples:
|
|
1132
|
+
${MANYOYO_NAME} run -r codex
|
|
1133
|
+
${MANYOYO_NAME} run --rm-on-exit -x /bin/bash -lc "node -v"
|
|
1134
|
+
${MANYOYO_NAME} run -n demo --first-shell "npm ci" -s "npm test"
|
|
1135
|
+
|
|
1136
|
+
Notes:
|
|
1137
|
+
参数优先级与合并规则(标量覆盖、数组追加、env 按 key 合并)请用 ${MANYOYO_NAME} config show --help 或查看文档。
|
|
1138
|
+
`);
|
|
991
1139
|
applyRunStyleOptions(runCommand);
|
|
992
1140
|
runCommand.action(options => selectAction('run', options));
|
|
993
1141
|
|
|
@@ -1069,7 +1217,7 @@ async function setupCommander() {
|
|
|
1069
1217
|
.action(() => selectAction('update', { update: true }));
|
|
1070
1218
|
|
|
1071
1219
|
program.command('install <name>')
|
|
1072
|
-
.description(
|
|
1220
|
+
.description(`安装 ${MANYOYO_NAME} 命令 (docker-cli-plugin)`)
|
|
1073
1221
|
.action(name => selectAction('install', { install: name }));
|
|
1074
1222
|
|
|
1075
1223
|
program.command('prune')
|
|
@@ -1184,15 +1332,15 @@ async function setupCommander() {
|
|
|
1184
1332
|
if (mergedShellSuffix) {
|
|
1185
1333
|
EXEC_COMMAND_SUFFIX = normalizeCommandSuffix(mergedShellSuffix);
|
|
1186
1334
|
}
|
|
1187
|
-
const mergedFirstShellPrefix = pickConfigValue(runFirstConfig.shellPrefix, globalFirstConfig.shellPrefix);
|
|
1335
|
+
const mergedFirstShellPrefix = pickConfigValue(options.firstShellPrefix, runFirstConfig.shellPrefix, globalFirstConfig.shellPrefix);
|
|
1188
1336
|
if (mergedFirstShellPrefix) {
|
|
1189
1337
|
FIRST_EXEC_COMMAND_PREFIX = `${mergedFirstShellPrefix} `;
|
|
1190
1338
|
}
|
|
1191
|
-
const mergedFirstShell = pickConfigValue(runFirstConfig.shell, globalFirstConfig.shell);
|
|
1339
|
+
const mergedFirstShell = pickConfigValue(options.firstShell, runFirstConfig.shell, globalFirstConfig.shell);
|
|
1192
1340
|
if (mergedFirstShell) {
|
|
1193
1341
|
FIRST_EXEC_COMMAND = mergedFirstShell;
|
|
1194
1342
|
}
|
|
1195
|
-
const mergedFirstShellSuffix = pickConfigValue(runFirstConfig.shellSuffix, globalFirstConfig.shellSuffix);
|
|
1343
|
+
const mergedFirstShellSuffix = pickConfigValue(options.firstShellSuffix, runFirstConfig.shellSuffix, globalFirstConfig.shellSuffix);
|
|
1196
1344
|
if (mergedFirstShellSuffix) {
|
|
1197
1345
|
FIRST_EXEC_COMMAND_SUFFIX = normalizeCommandSuffix(mergedFirstShellSuffix);
|
|
1198
1346
|
}
|
|
@@ -1221,13 +1369,15 @@ async function setupCommander() {
|
|
|
1221
1369
|
|
|
1222
1370
|
const firstEnvFileList = [
|
|
1223
1371
|
...toArray(globalFirstConfig.envFile),
|
|
1224
|
-
...toArray(runFirstConfig.envFile)
|
|
1372
|
+
...toArray(runFirstConfig.envFile),
|
|
1373
|
+
...(options.firstEnvFile || [])
|
|
1225
1374
|
].filter(Boolean);
|
|
1226
1375
|
firstEnvFileList.forEach(ef => addEnvFileTo(FIRST_CONTAINER_ENVS, ef));
|
|
1227
1376
|
|
|
1228
1377
|
const firstEnvMap = {
|
|
1229
1378
|
...normalizeJsonEnvMap(globalFirstConfig.env, '全局配置 first'),
|
|
1230
|
-
...normalizeJsonEnvMap(runFirstConfig.env, '运行配置 first')
|
|
1379
|
+
...normalizeJsonEnvMap(runFirstConfig.env, '运行配置 first'),
|
|
1380
|
+
...normalizeCliEnvMap(options.firstEnv)
|
|
1231
1381
|
};
|
|
1232
1382
|
Object.entries(firstEnvMap).forEach(([key, value]) => addEnvTo(FIRST_CONTAINER_ENVS, `${key}=${value}`));
|
|
1233
1383
|
|
|
@@ -1378,7 +1528,8 @@ function createRuntimeContext(modeState = {}) {
|
|
|
1378
1528
|
serverPort: SERVER_PORT,
|
|
1379
1529
|
serverAuthUser: SERVER_AUTH_USER,
|
|
1380
1530
|
serverAuthPass: SERVER_AUTH_PASS,
|
|
1381
|
-
serverAuthPassAuto: SERVER_AUTH_PASS_AUTO
|
|
1531
|
+
serverAuthPassAuto: SERVER_AUTH_PASS_AUTO,
|
|
1532
|
+
logger: null
|
|
1382
1533
|
};
|
|
1383
1534
|
}
|
|
1384
1535
|
|
|
@@ -1716,7 +1867,8 @@ async function runWebServerMode(runtime) {
|
|
|
1716
1867
|
BLUE,
|
|
1717
1868
|
CYAN,
|
|
1718
1869
|
NC
|
|
1719
|
-
}
|
|
1870
|
+
},
|
|
1871
|
+
logger: runtime.logger
|
|
1720
1872
|
});
|
|
1721
1873
|
}
|
|
1722
1874
|
|
|
@@ -1740,6 +1892,16 @@ async function main() {
|
|
|
1740
1892
|
|
|
1741
1893
|
// 2. Start web server mode
|
|
1742
1894
|
if (runtime.serverMode) {
|
|
1895
|
+
const serveLogger = createServeLogger();
|
|
1896
|
+
runtime.logger = serveLogger;
|
|
1897
|
+
installServeProcessDiagnostics(serveLogger);
|
|
1898
|
+
serveLogger.info('serve startup requested', {
|
|
1899
|
+
host: runtime.serverHost,
|
|
1900
|
+
port: runtime.serverPort,
|
|
1901
|
+
user: runtime.serverAuthUser || 'admin(auto/default)',
|
|
1902
|
+
process: getServeProcessSnapshot()
|
|
1903
|
+
});
|
|
1904
|
+
console.log(`${CYAN}📝 serve 日志文件: ${YELLOW}${serveLogger.path}${NC}`);
|
|
1743
1905
|
await runWebServerMode(runtime);
|
|
1744
1906
|
return;
|
|
1745
1907
|
}
|
package/lib/log-path.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
function pad2(n) {
|
|
5
|
+
return String(n).padStart(2, '0');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function getLocalDateTag(date = new Date()) {
|
|
9
|
+
const y = date.getFullYear();
|
|
10
|
+
const m = pad2(date.getMonth() + 1);
|
|
11
|
+
const d = pad2(date.getDate());
|
|
12
|
+
return `${y}-${m}-${d}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeLogScope(scope) {
|
|
16
|
+
return String(scope || 'general')
|
|
17
|
+
.trim()
|
|
18
|
+
.replace(/[^A-Za-z0-9_.-]+/g, '-')
|
|
19
|
+
.replace(/^-+|-+$/g, '') || 'general';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function buildManyoyoLogPath(scope, date = new Date(), homeDir = os.homedir()) {
|
|
23
|
+
const safeScope = normalizeLogScope(scope);
|
|
24
|
+
const rootDir = path.join(homeDir, '.manyoyo', 'logs');
|
|
25
|
+
const dir = path.join(rootDir, safeScope);
|
|
26
|
+
const file = `${safeScope}-${getLocalDateTag(date)}.log`;
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
rootDir,
|
|
30
|
+
dir,
|
|
31
|
+
path: path.join(dir, file),
|
|
32
|
+
scope: safeScope,
|
|
33
|
+
file
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = {
|
|
38
|
+
getLocalDateTag,
|
|
39
|
+
normalizeLogScope,
|
|
40
|
+
buildManyoyoLogPath
|
|
41
|
+
};
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xcanwin/manyoyo",
|
|
3
|
-
"version": "5.3.
|
|
3
|
+
"version": "5.3.6",
|
|
4
4
|
"imageVersion": "1.8.4-common",
|
|
5
5
|
"description": "AI Agent CLI Security Sandbox for Docker and Podman",
|
|
6
6
|
"keywords": [
|
|
@@ -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",
|