@xcanwin/manyoyo 8.0.1 → 8.2.0
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 +31 -177
- package/bin/manyoyo.js +149 -182
- package/lib/app-update.js +31 -10
- package/lib/podman-passthrough.js +4 -4
- package/lib/web/AGENTS.md +4 -6
- package/lib/web/{frontend/shadcn.html → index.html} +74 -74
- package/lib/web/server.js +14 -39
- package/manyoyo.example.json +1 -1
- package/package.json +15 -10
- package/lib/dev-release.js +0 -142
package/bin/manyoyo.js
CHANGED
|
@@ -7,7 +7,7 @@ const os = require('os');
|
|
|
7
7
|
const crypto = require('crypto');
|
|
8
8
|
const net = require('net');
|
|
9
9
|
const readline = require('readline');
|
|
10
|
-
const { Command } = require('commander');
|
|
10
|
+
const { Command, Help } = require('commander');
|
|
11
11
|
const { startWebServer } = require('../lib/web/server');
|
|
12
12
|
const { buildContainerRunArgs, buildContainerRunCommand } = require('../lib/container-run');
|
|
13
13
|
const { getManyoyoConfigPath, readManyoyoConfig, syncGlobalImageVersion } = require('../lib/global-config');
|
|
@@ -50,8 +50,7 @@ const {
|
|
|
50
50
|
} = require('../lib/serve-log');
|
|
51
51
|
const { version: BIN_VERSION, imageVersion: IMAGE_VERSION_DEFAULT } = require('../package.json');
|
|
52
52
|
const IMAGE_VERSION_BASE = String(IMAGE_VERSION_DEFAULT || '1.0.0').split('-')[0];
|
|
53
|
-
const
|
|
54
|
-
const IMAGE_VERSION_HELP_EXAMPLE = GLOBAL_CONFIG_IMAGE_VERSION || IMAGE_VERSION_DEFAULT || `${IMAGE_VERSION_BASE}-common`;
|
|
53
|
+
const IMAGE_VERSION_HELP_EXAMPLE = IMAGE_VERSION_DEFAULT || `${IMAGE_VERSION_BASE}-common`;
|
|
55
54
|
|
|
56
55
|
// Helper function to format date like bash $(date +%m%d-%H%M)
|
|
57
56
|
function formatDate() {
|
|
@@ -71,11 +70,6 @@ function detectCommandName() {
|
|
|
71
70
|
const rawArgv1 = process.argv[1] || '';
|
|
72
71
|
const baseName = path.basename(rawArgv1).replace(/\.(cjs|mjs|js)$/i, '');
|
|
73
72
|
|
|
74
|
-
if (baseName === 'docker-manyoyo') {
|
|
75
|
-
const pluginCommand = String(process.argv[2] || '').trim();
|
|
76
|
-
return pluginCommand || 'manyoyo';
|
|
77
|
-
}
|
|
78
|
-
|
|
79
73
|
return baseName || 'manyoyo';
|
|
80
74
|
}
|
|
81
75
|
|
|
@@ -472,7 +466,7 @@ function parseImageVersionTag(version) {
|
|
|
472
466
|
function validateImageVersion(value) {
|
|
473
467
|
validateName('imageVersion', value, /^[A-Za-z0-9][A-Za-z0-9_.-]*$/);
|
|
474
468
|
if (!parseImageVersionTag(value)) {
|
|
475
|
-
console.error(`${RED}⚠️ 错误: imageVersion 格式必须为 <x.y.z-后缀>,例如
|
|
469
|
+
console.error(`${RED}⚠️ 错误: imageVersion 格式必须为 <x.y.z-后缀>,例如 ${IMAGE_VERSION_HELP_EXAMPLE}。当前值: ${value}${NC}`);
|
|
476
470
|
process.exit(1);
|
|
477
471
|
}
|
|
478
472
|
}
|
|
@@ -849,24 +843,6 @@ async function ensureDocker(configuredRuntime, options = {}) {
|
|
|
849
843
|
return true;
|
|
850
844
|
}
|
|
851
845
|
|
|
852
|
-
function installManyoyo(name) {
|
|
853
|
-
const MANYOYO_FILE = fs.realpathSync(__filename);
|
|
854
|
-
switch (name) {
|
|
855
|
-
case 'docker-cli-plugin':
|
|
856
|
-
const pluginDir = path.join(process.env.HOME, '.docker/cli-plugins');
|
|
857
|
-
fs.mkdirSync(pluginDir, { recursive: true });
|
|
858
|
-
const targetPath = path.join(pluginDir, 'docker-manyoyo');
|
|
859
|
-
if (fs.existsSync(targetPath)) {
|
|
860
|
-
fs.unlinkSync(targetPath);
|
|
861
|
-
}
|
|
862
|
-
fs.symlinkSync(MANYOYO_FILE, targetPath);
|
|
863
|
-
break;
|
|
864
|
-
default:
|
|
865
|
-
console.log("");
|
|
866
|
-
}
|
|
867
|
-
process.exit(0);
|
|
868
|
-
}
|
|
869
|
-
|
|
870
846
|
// 升级后让旧版本的后台服务退出(pid 必须确实是 manyoyo serve),下次执行 manyoyo 就会启动新版本
|
|
871
847
|
function stopBackgroundApp({ quiet = false } = {}) {
|
|
872
848
|
const pid = readPid(path.join(os.homedir(), '.manyoyo', 'serve', 'app.json'));
|
|
@@ -918,16 +894,8 @@ async function updateOfflineInstall(mode, options, globalConfig) {
|
|
|
918
894
|
console.log(`${GREEN}✅ 更新完成: ${installed} → ${result.version}(上一版本 ${result.previous || '无'} 已保留,可用 ${MANYOYO_NAME} update --rollback 回滚)${NC}`);
|
|
919
895
|
|
|
920
896
|
// Podman / VM 磁盘有变化只提示(需要新的完整包)
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
if (release.assets[names.manifest]) {
|
|
924
|
-
const remote = JSON.parse(await appUpdate.fetchText(fetch, release.assets[names.manifest]));
|
|
925
|
-
const hint = appUpdate.describeRuntimeChange(appUpdate.readInstalledRecord(os.homedir()), remote);
|
|
926
|
-
if (hint) console.log(`${YELLOW}ℹ️ ${hint}${NC}`);
|
|
927
|
-
}
|
|
928
|
-
} catch (error) {
|
|
929
|
-
// 提示信息拿不到不影响升级
|
|
930
|
-
}
|
|
897
|
+
const runtimeHint = appUpdate.describeRuntimeChange(appUpdate.readInstalledRecord(os.homedir()), result.manifest.runtime);
|
|
898
|
+
if (runtimeHint && appUpdate.markRuntimeHintShown(os.homedir(), result.manifest.runtime)) console.log(`${YELLOW}ℹ️ ${runtimeHint}${NC}`);
|
|
931
899
|
|
|
932
900
|
// 新版本要求的镜像不存在就拉取;仍用旧镜像的容器只列出来,不动
|
|
933
901
|
const imageName = String(globalConfig.imageName || 'ghcr.io/xcanwin/manyoyo');
|
|
@@ -1102,26 +1070,6 @@ function pruneDanglingImages() {
|
|
|
1102
1070
|
});
|
|
1103
1071
|
}
|
|
1104
1072
|
|
|
1105
|
-
function maybeHandleDockerPluginMetadata(argv) {
|
|
1106
|
-
if (argv[2] !== 'docker-cli-plugin-metadata') {
|
|
1107
|
-
return false;
|
|
1108
|
-
}
|
|
1109
|
-
console.log(JSON.stringify({
|
|
1110
|
-
"SchemaVersion": "0.1.0",
|
|
1111
|
-
"Vendor": "xcanwin",
|
|
1112
|
-
"Version": "v1.0.0",
|
|
1113
|
-
"Description": "AI Agent CLI Sandbox"
|
|
1114
|
-
}, null, 4));
|
|
1115
|
-
return true;
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
function normalizeDockerPluginArgv(argv) {
|
|
1119
|
-
const dockerPluginPath = path.join(process.env.HOME || '', '.docker/cli-plugins/docker-manyoyo');
|
|
1120
|
-
if (argv[1] === dockerPluginPath && argv[2] === 'manyoyo') {
|
|
1121
|
-
argv.splice(2, 1);
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
1073
|
function normalizeShellFullArgv(argv) {
|
|
1126
1074
|
const shellFullIndex = argv.findIndex(arg => arg === '-x' || arg === '--shell-full');
|
|
1127
1075
|
if (shellFullIndex !== -1 && shellFullIndex < argv.length - 1) {
|
|
@@ -1183,7 +1131,7 @@ function applyRunStyleOptions(command, options = {}) {
|
|
|
1183
1131
|
.option('--cp, --cont-path <path>', '设置容器工作目录')
|
|
1184
1132
|
.option('-m, --cont-mode <mode>', '设置容器嵌套模式 (common, dind, sock; 注意: sock 模式可访问宿主机 Docker socket,风险较高)')
|
|
1185
1133
|
.option('--in, --image-name <name>', '指定镜像名称')
|
|
1186
|
-
.option('--iv, --image-ver <version>',
|
|
1134
|
+
.option('--iv, --image-ver <version>', `指定镜像版本 (格式: x.y.z-后缀,如 ${IMAGE_VERSION_HELP_EXAMPLE})`);
|
|
1187
1135
|
|
|
1188
1136
|
appendArrayOption(command, '-e, --env <env>', '设置环境变量 XXX=YYY (可多次使用)');
|
|
1189
1137
|
appendArrayOption(command, '--ef, --env-file <file>', '从环境文件加载变量 (仅支持绝对路径,如 /abs/path.env; 相对路径会报错)');
|
|
@@ -1200,7 +1148,7 @@ function applyRunStyleOptions(command, options = {}) {
|
|
|
1200
1148
|
.option('--first-shell <command>', '首次预执行命令 (仅新建容器生效; 容器已存在时忽略)')
|
|
1201
1149
|
.option('--first-shell-suffix <command>', '首次预执行命令后缀 (仅新建容器生效; 容器已存在时忽略)')
|
|
1202
1150
|
.option('-x, --shell-full <command...>', '完整命令 (与 --sp/-s/--ss/-- 互斥)')
|
|
1203
|
-
.option('-y, --yolo <cli>', '
|
|
1151
|
+
.option('-y, --yolo <cli>', '以免确认模式启动 Agent: c=Claude, cx=Codex, gm=Gemini, oc=OpenCode');
|
|
1204
1152
|
appendArrayOption(command, '--first-env <env>', '首次预执行环境变量 XXX=YYY (可多次使用)');
|
|
1205
1153
|
appendArrayOption(command, '--first-env-file <file>', '首次预执行环境变量文件 (仅支持绝对路径,如 /abs/path.env)');
|
|
1206
1154
|
|
|
@@ -1208,7 +1156,7 @@ function applyRunStyleOptions(command, options = {}) {
|
|
|
1208
1156
|
command.option('--rm-on-exit', '退出后自动删除容器 (一次性模式)');
|
|
1209
1157
|
}
|
|
1210
1158
|
|
|
1211
|
-
appendArrayOption(command, '-q, --quiet <item>', '
|
|
1159
|
+
appendArrayOption(command, '-q, --quiet <item>', '隐藏部分输出 (可多次使用,可选项见下方“-q 可选项”)');
|
|
1212
1160
|
|
|
1213
1161
|
if (includeServePreview) {
|
|
1214
1162
|
command
|
|
@@ -1313,46 +1261,82 @@ async function setupCommander() {
|
|
|
1313
1261
|
|
|
1314
1262
|
program
|
|
1315
1263
|
.name(MANYOYO_NAME)
|
|
1264
|
+
.optionsGroup('选项:')
|
|
1265
|
+
.commandsGroup('其他:')
|
|
1316
1266
|
.version(BIN_VERSION, '-v, --version', '显示版本')
|
|
1317
|
-
.
|
|
1267
|
+
.helpOption('-h, --help', '显示帮助')
|
|
1268
|
+
.helpCommand('help [command]', '显示指定命令的帮助')
|
|
1269
|
+
.configureHelp({
|
|
1270
|
+
optionDescription(option) {
|
|
1271
|
+
return Help.prototype.optionDescription.call(this, option).replace(/\s*\(default: \[\]\)/, '');
|
|
1272
|
+
}
|
|
1273
|
+
})
|
|
1274
|
+
.description(`MANYOYO - AI Agent CLI Sandbox
|
|
1275
|
+
https://github.com/xcanwin/manyoyo
|
|
1276
|
+
|
|
1277
|
+
不带参数直接运行,打开网页界面(首次使用进入配置向导):
|
|
1278
|
+
${MANYOYO_NAME} 自动判断有无图形界面
|
|
1279
|
+
${MANYOYO_NAME} --headless 强制按无图形界面处理(不打开浏览器,用密码登录)
|
|
1280
|
+
${MANYOYO_NAME} --gui 强制按有图形界面处理(自动打开浏览器)`)
|
|
1318
1281
|
.addHelpText('after', `
|
|
1319
|
-
配置文件:
|
|
1320
|
-
~/.manyoyo/manyoyo.json 全局配置文件 (JSON5格式,支持注释)
|
|
1321
|
-
~/.manyoyo/run/c.json 运行配置示例
|
|
1322
|
-
|
|
1323
|
-
路径规则:
|
|
1324
|
-
run -r name → ~/.manyoyo/manyoyo.json 的 runs.name
|
|
1325
|
-
run --ef /abs/path.env → 绝对路径环境文件
|
|
1326
|
-
run --ss "<args>" → 显式设置命令后缀
|
|
1327
|
-
run -- <args...> → 直接透传命令后缀(优先级最高)
|
|
1328
|
-
|
|
1329
1282
|
示例:
|
|
1330
|
-
${MANYOYO_NAME}
|
|
1331
|
-
${MANYOYO_NAME}
|
|
1332
|
-
${MANYOYO_NAME}
|
|
1333
|
-
${MANYOYO_NAME}
|
|
1334
|
-
${MANYOYO_NAME}
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
${MANYOYO_NAME} run -x "echo 123" 使用完整命令
|
|
1339
|
-
${MANYOYO_NAME} serve 127.0.0.1:3000 启动本机网页服务
|
|
1340
|
-
${MANYOYO_NAME} serve 127.0.0.1:3000 -d 后台启动;未设密码时会打印本次随机密码
|
|
1341
|
-
${MANYOYO_NAME} serve 0.0.0.0:3000 -U admin -P 123 -d 后台启动并监听全部网卡
|
|
1342
|
-
${MANYOYO_NAME} serve 0.0.0.0:3000 -U admin -P 123 -d --restart 重启指定后台网页服务
|
|
1343
|
-
${MANYOYO_NAME} playwright up mcp-host-headless 启动 playwright MCP 宿主场景(默认/推荐)
|
|
1344
|
-
${MANYOYO_NAME} playwright up cli-host-headless 启动 playwright CLI 宿主场景(供容器内 playwright-cli 附着)
|
|
1345
|
-
${MANYOYO_NAME} run -n test -q tip -q cmd 多次使用静默选项
|
|
1283
|
+
${MANYOYO_NAME} 打开网页界面
|
|
1284
|
+
${MANYOYO_NAME} init all 导入本机已有的 Agent 配置
|
|
1285
|
+
${MANYOYO_NAME} run -y c 在沙箱里以免确认模式启动 Claude Code
|
|
1286
|
+
${MANYOYO_NAME} run -r claude 使用 manyoyo.json 的 runs.claude 启动
|
|
1287
|
+
${MANYOYO_NAME} update 升级到最新版本
|
|
1288
|
+
|
|
1289
|
+
配置文件: ~/.manyoyo/manyoyo.json (JSON5 格式,支持注释)
|
|
1290
|
+
各命令的更多用法: ${MANYOYO_NAME} <命令> --help
|
|
1346
1291
|
`);
|
|
1347
1292
|
|
|
1348
|
-
|
|
1293
|
+
// 日常
|
|
1294
|
+
program.command('update')
|
|
1295
|
+
.helpGroup('日常:')
|
|
1296
|
+
.description('升级到最新版本')
|
|
1297
|
+
.option('--rollback', '回到上一版本')
|
|
1298
|
+
.action(options => selectAction('update', { update: true, rollback: Boolean(options.rollback) }));
|
|
1299
|
+
|
|
1300
|
+
program.command('uninstall')
|
|
1301
|
+
.helpGroup('日常:')
|
|
1302
|
+
.description('卸载 MANYOYO(配置和数据默认保留)')
|
|
1303
|
+
.option('--yes', '确认卸载程序本身,不再询问(不会删除配置、历史、日志、工作目录和外部运行时里的容器镜像)')
|
|
1304
|
+
.action(options => selectAction('uninstall', options));
|
|
1305
|
+
|
|
1306
|
+
program.command('setup')
|
|
1307
|
+
.helpGroup('日常:')
|
|
1308
|
+
.description('命令行配置向导(无图形界面时使用)')
|
|
1309
|
+
.action(() => selectAction('setup', {}));
|
|
1310
|
+
|
|
1311
|
+
program.command('doctor')
|
|
1312
|
+
.helpGroup('日常:')
|
|
1313
|
+
.description('诊断容器运行时、镜像、配置和端口')
|
|
1314
|
+
.option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)')
|
|
1315
|
+
.option('--port <port>', '检查指定监听端口')
|
|
1316
|
+
.option('--json', '以 JSON 输出稳定诊断结果')
|
|
1317
|
+
.option('--fix', '自动修复可修复项(启动容器环境、拉取镜像、生成默认配置,端口占用时给出建议端口)')
|
|
1318
|
+
.action(options => selectAction('doctor', { ...options, doctor: true }));
|
|
1319
|
+
|
|
1320
|
+
// 命令行运行
|
|
1321
|
+
const runCommand = program.command('run').helpGroup('命令行运行:').description('启动容器并运行命令(容器已存在则连接)');
|
|
1349
1322
|
runCommand.addHelpText('after', `
|
|
1350
|
-
|
|
1323
|
+
示例:
|
|
1351
1324
|
${MANYOYO_NAME} run -r codex
|
|
1325
|
+
${MANYOYO_NAME} run -n test --ef /path/ab.env -y c
|
|
1326
|
+
${MANYOYO_NAME} run -n test -- -c
|
|
1327
|
+
${MANYOYO_NAME} run -r codex --ss "resume --last"
|
|
1352
1328
|
${MANYOYO_NAME} run --rm-on-exit -x /bin/bash -lc "node -v"
|
|
1353
1329
|
${MANYOYO_NAME} run -n demo --first-shell "npm ci" -s "npm test"
|
|
1354
1330
|
|
|
1355
|
-
|
|
1331
|
+
-q 可选项(可多次使用):
|
|
1332
|
+
cnew 隐藏“创建/连接容器”提示
|
|
1333
|
+
crm 隐藏“删除容器”提示
|
|
1334
|
+
tip 隐藏首次命令与恢复会话提示
|
|
1335
|
+
cmd 隐藏将执行的命令
|
|
1336
|
+
askkeep 简化“是否保留容器”的提问
|
|
1337
|
+
full 隐藏以上全部
|
|
1338
|
+
|
|
1339
|
+
说明:
|
|
1356
1340
|
参数优先级与合并规则(标量覆盖、数组追加、env 按 key 合并)请用 ${MANYOYO_NAME} config show --help 或查看文档。
|
|
1357
1341
|
`);
|
|
1358
1342
|
applyRunStyleOptions(runCommand);
|
|
@@ -1362,60 +1346,13 @@ Notes:
|
|
|
1362
1346
|
selectAction('run', options);
|
|
1363
1347
|
});
|
|
1364
1348
|
|
|
1365
|
-
const
|
|
1366
|
-
|
|
1367
|
-
.option('
|
|
1368
|
-
.
|
|
1369
|
-
.option('--iv, --image-ver <version>', '指定镜像版本 (格式: x.y.z-后缀,如 1.7.4-common)')
|
|
1370
|
-
.option('--update-agents', '仅更新已有镜像内 Agent CLI 到 latest (Claude/Codex/Gemini/OpenCode)')
|
|
1371
|
-
.option('--yes', '所有提示自动确认 (用于CI/脚本)');
|
|
1372
|
-
appendArrayOption(buildCommand, '--iba, --image-build-arg <arg>', '构建镜像时传参给dockerfile (可多次使用)');
|
|
1373
|
-
buildCommand.action(options => selectAction('build', options));
|
|
1374
|
-
|
|
1375
|
-
const removeCommand = program.command('rm <name>').description('删除指定容器');
|
|
1376
|
-
removeCommand
|
|
1377
|
-
.option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)')
|
|
1378
|
-
.action((name, options) => selectAction('rm', { ...options, contName: name }));
|
|
1379
|
-
|
|
1380
|
-
program.command('ps')
|
|
1381
|
-
.description('列举容器')
|
|
1382
|
-
.action(() => selectAction('ps', { contList: true }));
|
|
1383
|
-
|
|
1384
|
-
program.command('images')
|
|
1385
|
-
.description('列举镜像')
|
|
1386
|
-
.action(() => selectAction('images', { imageList: true }));
|
|
1387
|
-
|
|
1388
|
-
const serveCommand = program.command('serve [listen]').description('启动网页交互服务 (默认 127.0.0.1:3000)');
|
|
1389
|
-
applyRunStyleOptions(serveCommand, { includeRmOnExit: false, includeWebAuthOptions: true });
|
|
1390
|
-
serveCommand.option('-d, --detach', '后台启动网页服务并立即返回');
|
|
1391
|
-
serveCommand.option('--stop', '停止后台网页服务;必须显式传入 listen');
|
|
1392
|
-
serveCommand.option('--restart', '重启后台网页服务;必须显式传入 listen');
|
|
1393
|
-
serveCommand.action((listen, options) => {
|
|
1394
|
-
selectAction('serve', {
|
|
1395
|
-
...options,
|
|
1396
|
-
server: listen === undefined ? true : listen,
|
|
1397
|
-
serverUser: options.user,
|
|
1398
|
-
serverPass: options.pass
|
|
1399
|
-
});
|
|
1400
|
-
});
|
|
1401
|
-
|
|
1402
|
-
const playwrightCommand = program.command('playwright').description('管理 playwright 插件服务(推荐)');
|
|
1403
|
-
registerPlaywrightAliasCommands(playwrightCommand);
|
|
1404
|
-
|
|
1405
|
-
const pluginCommand = program.command('plugin').description('管理 manyoyo 插件');
|
|
1406
|
-
pluginCommand.command('ls')
|
|
1407
|
-
.description('列出可用插件与启用场景')
|
|
1408
|
-
.option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)')
|
|
1409
|
-
.action(options => selectPluginAction({
|
|
1410
|
-
action: 'ls',
|
|
1411
|
-
pluginName: 'playwright',
|
|
1412
|
-
scene: 'all'
|
|
1413
|
-
}, options));
|
|
1414
|
-
const pluginPlaywrightCommand = pluginCommand.command('playwright').description('管理 playwright 插件服务');
|
|
1415
|
-
registerPlaywrightAliasCommands(pluginPlaywrightCommand);
|
|
1349
|
+
const initCommand = program.command('init [agents]').helpGroup('命令行运行:').description('导入本机已有的 Agent 配置到 ~/.manyoyo');
|
|
1350
|
+
initCommand
|
|
1351
|
+
.option('--yes', '所有提示自动确认 (用于CI/脚本)')
|
|
1352
|
+
.action((agents, options) => selectAction('init', { ...options, initConfig: agents === undefined ? 'all' : agents }));
|
|
1416
1353
|
|
|
1417
|
-
const configCommand = program.command('config').description('
|
|
1418
|
-
const configShowCommand = configCommand.command('show').description('
|
|
1354
|
+
const configCommand = program.command('config').helpGroup('命令行运行:').description('查看生效的配置或将执行的命令');
|
|
1355
|
+
const configShowCommand = configCommand.command('show').description('显示最终生效的配置');
|
|
1419
1356
|
applyRunStyleOptions(configShowCommand, { includeRmOnExit: false, includeServePreview: true });
|
|
1420
1357
|
enableShellSuffixPassThrough(configShowCommand);
|
|
1421
1358
|
configShowCommand.action((options, command) => {
|
|
@@ -1432,7 +1369,7 @@ Notes:
|
|
|
1432
1369
|
selectAction('config-show', finalOptions);
|
|
1433
1370
|
});
|
|
1434
1371
|
|
|
1435
|
-
const configRunCommand = configCommand.command('command').description('
|
|
1372
|
+
const configRunCommand = configCommand.command('command').description('显示将执行的容器命令');
|
|
1436
1373
|
applyRunStyleOptions(configRunCommand, { includeRmOnExit: false });
|
|
1437
1374
|
enableShellSuffixPassThrough(configRunCommand);
|
|
1438
1375
|
configRunCommand.action((options, command) => {
|
|
@@ -1440,55 +1377,86 @@ Notes:
|
|
|
1440
1377
|
selectAction('config-command', options);
|
|
1441
1378
|
});
|
|
1442
1379
|
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
.
|
|
1446
|
-
.
|
|
1380
|
+
// 容器与镜像
|
|
1381
|
+
program.command('ps')
|
|
1382
|
+
.helpGroup('容器与镜像:')
|
|
1383
|
+
.description('列出容器')
|
|
1384
|
+
.action(() => selectAction('ps', { contList: true }));
|
|
1447
1385
|
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
.
|
|
1451
|
-
.action((
|
|
1386
|
+
program.command('images')
|
|
1387
|
+
.helpGroup('容器与镜像:')
|
|
1388
|
+
.description('列出镜像')
|
|
1389
|
+
.action(() => selectAction('images', { imageList: true }));
|
|
1452
1390
|
|
|
1453
|
-
program.command('
|
|
1454
|
-
|
|
1391
|
+
const removeCommand = program.command('rm <name>').helpGroup('容器与镜像:').description('删除指定容器');
|
|
1392
|
+
removeCommand
|
|
1455
1393
|
.option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)')
|
|
1456
|
-
.
|
|
1457
|
-
.option('--json', '以 JSON 输出稳定诊断结果')
|
|
1458
|
-
.option('--fix', '自动修复可修复项(启动容器环境、拉取镜像、生成默认配置,端口占用时给出建议端口)')
|
|
1459
|
-
.action(options => selectAction('doctor', { ...options, doctor: true }));
|
|
1394
|
+
.action((name, options) => selectAction('rm', { ...options, contName: name }));
|
|
1460
1395
|
|
|
1461
|
-
program.command('
|
|
1462
|
-
|
|
1463
|
-
.option('--
|
|
1464
|
-
.
|
|
1396
|
+
const buildCommand = program.command('build').helpGroup('容器与镜像:').description('构建沙箱镜像');
|
|
1397
|
+
buildCommand
|
|
1398
|
+
.option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)')
|
|
1399
|
+
.option('--in, --image-name <name>', '指定镜像名称')
|
|
1400
|
+
.option('--iv, --image-ver <version>', `指定镜像版本 (格式: x.y.z-后缀,如 ${IMAGE_VERSION_HELP_EXAMPLE})`)
|
|
1401
|
+
.option('--update-agents', '仅更新已有镜像内 Agent CLI 到 latest (Claude/Codex/Gemini/OpenCode)')
|
|
1402
|
+
.option('--yes', '所有提示自动确认 (用于CI/脚本)');
|
|
1403
|
+
appendArrayOption(buildCommand, '--iba, --image-build-arg <arg>', '构建镜像时传参给dockerfile (可多次使用)');
|
|
1404
|
+
buildCommand.action(options => selectAction('build', options));
|
|
1405
|
+
|
|
1406
|
+
program.command('prune')
|
|
1407
|
+
.helpGroup('容器与镜像:')
|
|
1408
|
+
.description('清理悬空镜像')
|
|
1409
|
+
.action(() => selectAction('prune', { imageRemove: true }));
|
|
1465
1410
|
|
|
1466
1411
|
program.command('podman [args...]')
|
|
1467
|
-
.
|
|
1412
|
+
.helpGroup('容器与镜像:')
|
|
1413
|
+
.description('用私有 Podman 执行命令(参数原样传入)')
|
|
1468
1414
|
.helpOption(false)
|
|
1469
1415
|
.allowUnknownOption()
|
|
1470
1416
|
.passThroughOptions()
|
|
1471
1417
|
.action(args => selectAction('podman', { podmanArgs: args || [] }));
|
|
1472
1418
|
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1419
|
+
// 网页服务与插件
|
|
1420
|
+
const serveCommand = program.command('serve [listen]').helpGroup('网页服务与插件:').description('启动网页服务 (默认 127.0.0.1:3000)');
|
|
1421
|
+
serveCommand.addHelpText('after', `
|
|
1422
|
+
示例:
|
|
1423
|
+
${MANYOYO_NAME} serve 127.0.0.1:3000 启动本机网页服务
|
|
1424
|
+
${MANYOYO_NAME} serve 127.0.0.1:3000 -d 后台启动;未设密码时会打印本次随机密码
|
|
1425
|
+
${MANYOYO_NAME} serve 0.0.0.0:3000 -U admin -P <密码> -d 后台启动并监听全部网卡
|
|
1426
|
+
${MANYOYO_NAME} serve 0.0.0.0:3000 -U admin -P <密码> -d --restart 重启指定后台网页服务
|
|
1427
|
+
`);
|
|
1428
|
+
applyRunStyleOptions(serveCommand, { includeRmOnExit: false, includeWebAuthOptions: true });
|
|
1429
|
+
serveCommand.option('-d, --detach', '后台启动网页服务并立即返回');
|
|
1430
|
+
serveCommand.option('--stop', '停止后台网页服务;必须显式传入 listen');
|
|
1431
|
+
serveCommand.option('--restart', '重启后台网页服务;必须显式传入 listen');
|
|
1432
|
+
serveCommand.action((listen, options) => {
|
|
1433
|
+
selectAction('serve', {
|
|
1434
|
+
...options,
|
|
1435
|
+
server: listen === undefined ? true : listen,
|
|
1436
|
+
serverUser: options.user,
|
|
1437
|
+
serverPass: options.pass
|
|
1438
|
+
});
|
|
1439
|
+
});
|
|
1484
1440
|
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1441
|
+
const playwrightCommand = program.command('playwright').helpGroup('网页服务与插件:').description('管理 Playwright 插件服务');
|
|
1442
|
+
playwrightCommand.addHelpText('after', `
|
|
1443
|
+
示例:
|
|
1444
|
+
${MANYOYO_NAME} playwright up mcp-host-headless 启动 MCP 宿主场景(默认/推荐)
|
|
1445
|
+
${MANYOYO_NAME} playwright up cli-host-headless 启动 CLI 宿主场景(供容器内 playwright-cli 附着)
|
|
1446
|
+
`);
|
|
1447
|
+
registerPlaywrightAliasCommands(playwrightCommand);
|
|
1489
1448
|
|
|
1490
|
-
|
|
1491
|
-
|
|
1449
|
+
const pluginCommand = program.command('plugin', { hidden: true }).description('管理 manyoyo 插件');
|
|
1450
|
+
pluginCommand.command('ls')
|
|
1451
|
+
.description('列出可用插件与启用场景')
|
|
1452
|
+
.option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)')
|
|
1453
|
+
.action(options => selectPluginAction({
|
|
1454
|
+
action: 'ls',
|
|
1455
|
+
pluginName: 'playwright',
|
|
1456
|
+
scene: 'all'
|
|
1457
|
+
}, options));
|
|
1458
|
+
const pluginPlaywrightCommand = pluginCommand.command('playwright').description('管理 playwright 插件服务');
|
|
1459
|
+
registerPlaywrightAliasCommands(pluginPlaywrightCommand);
|
|
1492
1460
|
|
|
1493
1461
|
// No args (or only --headless / --gui): start (or reuse) the local web app; open the browser already logged in unless headless
|
|
1494
1462
|
const launcherArgs = parseLauncherArgs(process.argv.slice(2));
|
|
@@ -1528,7 +1496,7 @@ Notes:
|
|
|
1528
1496
|
|
|
1529
1497
|
UPDATE_CHECK_ENABLED = config.updateCheck !== false;
|
|
1530
1498
|
MIRRORS = normalizeMirrors(config.mirrors);
|
|
1531
|
-
const noDockerActions = new Set(['init', 'update', '
|
|
1499
|
+
const noDockerActions = new Set(['init', 'update', 'config-show', 'plugin', 'doctor', 'uninstall', 'podman', 'setup']);
|
|
1532
1500
|
if (isServerStopMode) {
|
|
1533
1501
|
noDockerActions.add('serve');
|
|
1534
1502
|
}
|
|
@@ -1831,7 +1799,6 @@ Notes:
|
|
|
1831
1799
|
if (isPsMode) { getContList(); process.exit(0); }
|
|
1832
1800
|
if (isImagesMode) { getImageList(); process.exit(0); }
|
|
1833
1801
|
if (isPruneMode) { pruneDanglingImages(); process.exit(0); }
|
|
1834
|
-
if (selectedAction === 'install') { installManyoyo(options.install); process.exit(0); }
|
|
1835
1802
|
|
|
1836
1803
|
return {
|
|
1837
1804
|
yesMode,
|
package/lib/app-update.js
CHANGED
|
@@ -125,11 +125,11 @@ function parseSha256Sums(text) {
|
|
|
125
125
|
return result;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
// Release 里只有一个校验清单 SHA256SUMS,列出全部安装包与升级包
|
|
128
129
|
function releaseAssetNames(version, targetArch, targetOs = 'macos') {
|
|
129
130
|
return {
|
|
130
131
|
app: `manyoyo-${version}-${targetOs}-${targetArch}-app.tar.gz`,
|
|
131
|
-
sums:
|
|
132
|
-
manifest: `release-manifest-${targetOs}-${targetArch}.json`
|
|
132
|
+
sums: 'SHA256SUMS'
|
|
133
133
|
};
|
|
134
134
|
}
|
|
135
135
|
|
|
@@ -189,7 +189,7 @@ function pruneApps(appRoot, keep) {
|
|
|
189
189
|
async function installAppUpdate({ appRoot, release, fetchImpl = fetch, targetArch = arch(), targetOs = 'macos', run = defaultRun, log = () => {}, tmpRoot = os.tmpdir() }) {
|
|
190
190
|
const names = releaseAssetNames(release.version, targetArch, targetOs);
|
|
191
191
|
const appUrl = release.assets[names.app];
|
|
192
|
-
const sumsUrl = release.assets[names.sums]
|
|
192
|
+
const sumsUrl = release.assets[names.sums];
|
|
193
193
|
if (!appUrl || !sumsUrl) {
|
|
194
194
|
throw new UpdateError('NO_ASSET', `Release ${release.version} 里没有适合这台机器(${targetOs}-${targetArch})的升级包(需要 ${names.app} 与 ${names.sums})。`);
|
|
195
195
|
}
|
|
@@ -262,20 +262,40 @@ function rollbackApp({ appRoot }) {
|
|
|
262
262
|
|
|
263
263
|
/**
|
|
264
264
|
* Podman / VM 磁盘版本变化只提示,不自动替换(需要下载新的完整包)。
|
|
265
|
-
* installed:安装器写的 ~/.manyoyo/.install/installed.json;
|
|
265
|
+
* installed:安装器写的 ~/.manyoyo/.install/installed.json;remoteRuntime:升级包 manifest.json 里的 runtime({ podmanVersion, vmDiskSha256 },仅 macOS 包有)。
|
|
266
266
|
*/
|
|
267
|
-
function describeRuntimeChange(installed,
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
const vmChanged = Boolean(installed.vmDiskSha256) && remote.vmDisk && remote.vmDisk.sha256 !== installed.vmDiskSha256;
|
|
267
|
+
function describeRuntimeChange(installed, remoteRuntime) {
|
|
268
|
+
if (!installed || !installed.podmanVersion || !remoteRuntime || !remoteRuntime.podmanVersion) return '';
|
|
269
|
+
const podmanChanged = remoteRuntime.podmanVersion !== installed.podmanVersion;
|
|
270
|
+
const vmChanged = Boolean(installed.vmDiskSha256) && Boolean(remoteRuntime.vmDiskSha256) && remoteRuntime.vmDiskSha256 !== installed.vmDiskSha256;
|
|
272
271
|
if (!podmanChanged && !vmChanged) return '';
|
|
273
272
|
const parts = [];
|
|
274
|
-
if (podmanChanged) parts.push(`Podman ${installed.podmanVersion} → ${
|
|
273
|
+
if (podmanChanged) parts.push(`Podman ${installed.podmanVersion} → ${remoteRuntime.podmanVersion}`);
|
|
275
274
|
if (vmChanged) parts.push('虚拟机磁盘有更新');
|
|
276
275
|
return `新版本捆绑的运行环境有变化(${parts.join(',')})。本次只升级了 manyoyo 本体;要换 Podman / 虚拟机,请下载新的完整安装包重新安装。`;
|
|
277
276
|
}
|
|
278
277
|
|
|
278
|
+
/**
|
|
279
|
+
* 同一份新运行环境的提示只显示一次(每次 update 都重复提示只是噪音):
|
|
280
|
+
* 记下已提示过的 runtime(~/.manyoyo/.install/runtime-hint.json),返回这次是否应该显示。
|
|
281
|
+
*/
|
|
282
|
+
function markRuntimeHintShown(homeDir, runtime) {
|
|
283
|
+
const file = path.join(homeDir, '.manyoyo', '.install', 'runtime-hint.json');
|
|
284
|
+
const key = JSON.stringify({ podmanVersion: runtime && runtime.podmanVersion || '', vmDiskSha256: runtime && runtime.vmDiskSha256 || '' });
|
|
285
|
+
try {
|
|
286
|
+
if (fs.readFileSync(file, 'utf-8').trim() === key) return false;
|
|
287
|
+
} catch (error) {
|
|
288
|
+
// 没有记录或记录损坏:当作没提示过
|
|
289
|
+
}
|
|
290
|
+
try {
|
|
291
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
292
|
+
fs.writeFileSync(file, `${key}\n`);
|
|
293
|
+
} catch (error) {
|
|
294
|
+
// 记录写不进去不影响升级,最多下次再提示一次
|
|
295
|
+
}
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
|
|
279
299
|
function readInstalledRecord(homeDir) {
|
|
280
300
|
try {
|
|
281
301
|
return JSON.parse(fs.readFileSync(path.join(homeDir, '.manyoyo', '.install', 'installed.json'), 'utf-8'));
|
|
@@ -317,6 +337,7 @@ module.exports = {
|
|
|
317
337
|
rollbackApp,
|
|
318
338
|
currentVersion,
|
|
319
339
|
describeRuntimeChange,
|
|
340
|
+
markRuntimeHintShown,
|
|
320
341
|
readInstalledRecord,
|
|
321
342
|
findOutdatedContainers,
|
|
322
343
|
fetchText,
|
|
@@ -48,14 +48,14 @@ function runPodmanCommand(args, options = {}) {
|
|
|
48
48
|
const commandName = options.commandName || 'manyoyo';
|
|
49
49
|
const paths = getPrivatePodmanPaths(options.homeDir);
|
|
50
50
|
const exists = options.exists || (file => require('fs').existsSync(file));
|
|
51
|
-
if (!exists(paths.bin)) {
|
|
52
|
-
errLog(`没有找到 ${commandName} 的私有 Podman(${paths.bin})。当前没有使用私有 Podman:离线完整包安装后才有;使用你自己的 docker / podman 时直接用它们即可。`);
|
|
53
|
-
return 1;
|
|
54
|
-
}
|
|
55
51
|
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
56
52
|
log(usageText(commandName));
|
|
57
53
|
return 0;
|
|
58
54
|
}
|
|
55
|
+
if (!exists(paths.bin)) {
|
|
56
|
+
errLog(`没有找到 ${commandName} 的私有 Podman(${paths.bin})。当前没有使用私有 Podman:离线完整包安装后才有;使用你自己的 docker / podman 时直接用它们即可。`);
|
|
57
|
+
return 1;
|
|
58
|
+
}
|
|
59
59
|
if (args[0] === 'env') {
|
|
60
60
|
const rest = args.slice(1);
|
|
61
61
|
let shell = '';
|
package/lib/web/AGENTS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# lib/web/ 协作指引
|
|
2
2
|
|
|
3
|
-
根目录 `AGENTS.md` 的补充,仅在改动 Web 服务端时适用。前端在 `frontend
|
|
3
|
+
根目录 `AGENTS.md` 的补充,仅在改动 Web 服务端时适用。前端在 `frontend/`,见该目录的 `AGENTS.md`。
|
|
4
4
|
|
|
5
5
|
## lib/web/server.js(6000+ 行单文件)
|
|
6
6
|
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
- `resolveYoloCommand()` 委托到 `lib/agent-adapters/index.js`,与 `bin/manyoyo.js` 共用同一份映射,无需分别维护。
|
|
10
10
|
- Agent 会话恢复参数:Claude/Gemini → `-r`,Codex → `resume`,OpenCode → `-c`。
|
|
11
11
|
- 会话控制事件:`/agent/stream`、`/agent/stop` 通过 `createWebStreamEmitter()` / `appendWebSessionControlEvent()` 写入 `lib/core/event-store.js`(`FileEventStore`),`GET /api/sessions/:name/audit` 导出该会话的事件与投影。
|
|
12
|
-
- **`/agent/stream` 的 NDJSON 事件协议是跨前后端的契约**,改一处必须同步另一处:服务端 `lib/web/server.js`、前端 `frontend
|
|
12
|
+
- **`/agent/stream` 的 NDJSON 事件协议是跨前后端的契约**,改一处必须同步另一处:服务端 `lib/web/server.js`、前端 `frontend/src/lib/api.ts` 的 `StreamEvent` + `workspace-panel.tsx` 的事件分支
|
|
13
13
|
- `content_chunk`:token 级增量,前端**追加**;`reset: true` 表示换了一条 assistant 消息、从空白重新开始。只发新增片段,不要改成重发累计全文(几千个增量就是 O(n²) 流量)。
|
|
14
14
|
- `content_delta`:每条 assistant 消息落地时下发一次的权威全文,前端**整体覆盖**。
|
|
15
15
|
- `trace`:**不保证带结构化 `traceEvent`**。stderr 行(`emitStderrTraceLine`)和解析不出 JSON 的 stdout 行只有 `text` + `stream`,前端靠 `toTraceEvent()` 兜底成 `kind="output"` 的事件展示。新增只认 `traceEvent` 的分支等于把自定义 CLI、CLI 报错的输出全吞掉。
|
|
@@ -21,11 +21,9 @@
|
|
|
21
21
|
- 空闲保活是 `/agent/stream` 那条心跳规则的同类问题,**两个入口都要守**:终端侧服务端每 `WEB_TERMINAL_PING_INTERVAL_MS`(30s)发 WebSocket ping 帧,连续 `WEB_TERMINAL_MAX_MISSED_PONGS`(3 次,约 90s)无 pong 才判死;前端另发应用层 `ping`(浏览器 JS 发不出 ping 帧,上行需要自己造流量)。判死阈值不要收紧到一个周期:手机切后台会让连接短暂挂起,误杀等于用户的 shell 没了。升级后的 socket 还要 `setTimeout(0)` + `setKeepAlive`,解除 HTTP 侧空闲超时。
|
|
22
22
|
- 新增接口/页面必须走全局认证网关,禁止在业务路由里零散补认证;匿名白名单见根 `AGENTS.md` 的安全约束。
|
|
23
23
|
|
|
24
|
-
## lib/web/
|
|
24
|
+
## lib/web/index.html
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
目录里没有任何被跟踪的文件,而 git 不记录空目录——**全新检出(CI、别人第一次 clone)是没有 `lib/web/frontend/` 这个目录的**。任何往这里写文件的构建步骤都必须自己 `mkdir -p`(见 `frontend-shadcn/scripts/emit-shadcn-html.mjs`),否则本地有目录跑得好好的,CI 上直接 ENOENT。
|
|
26
|
+
前端构建产物(`npm run build:web`,已 `.gitignore`),由 `loadIndexHtml()` 在每次请求时读取,`/` 与登录页 `/auth/login` 共用。服务端不托管其它前端静态资源,别往 `lib/web/` 里放散装 js/css。
|
|
29
27
|
|
|
30
28
|
## 测试
|
|
31
29
|
|