@xcanwin/manyoyo 5.1.10 → 5.2.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/bin/manyoyo.js CHANGED
@@ -56,8 +56,12 @@ let IMAGE_VERSION = IMAGE_VERSION_DEFAULT || `${IMAGE_VERSION_BASE}-common`;
56
56
  let EXEC_COMMAND = "";
57
57
  let EXEC_COMMAND_PREFIX = "";
58
58
  let EXEC_COMMAND_SUFFIX = "";
59
+ let FIRST_EXEC_COMMAND = "";
60
+ let FIRST_EXEC_COMMAND_PREFIX = "";
61
+ let FIRST_EXEC_COMMAND_SUFFIX = "";
59
62
  let IMAGE_BUILD_ARGS = [];
60
63
  let CONTAINER_ENVS = [];
64
+ let FIRST_CONTAINER_ENVS = [];
61
65
  let CONTAINER_VOLUMES = [];
62
66
  let CONTAINER_PORTS = [];
63
67
  const MANYOYO_NAME = detectCommandName();
@@ -236,6 +240,7 @@ function sanitizeSensitiveData(obj) {
236
240
  * @property {string} [imageVersion] - 镜像版本
237
241
  * @property {Object.<string, string|number|boolean>} [env] - 环境变量映射
238
242
  * @property {string[]} [envFile] - 环境文件数组
243
+ * @property {{shellPrefix?:string,shell?:string,shellSuffix?:string,env?:Object.<string,string|number|boolean>,envFile?:string[]}} [first] - 仅首次创建容器执行的一次性命令配置
239
244
  * @property {string[]} [volumes] - 挂载卷数组
240
245
  * @property {Object.<string, Object>} [plugins] - 可选插件配置映射(如 plugins.playwright)
241
246
  * @property {Object.<string, Object>} [runs] - 运行配置映射(-r <name>)
@@ -436,12 +441,27 @@ function normalizeCliEnvMap(envList) {
436
441
  return envMap;
437
442
  }
438
443
 
439
- function addEnv(env) {
444
+ function normalizeFirstConfig(firstConfig, sourceLabel) {
445
+ if (firstConfig === undefined || firstConfig === null) {
446
+ return {};
447
+ }
448
+ if (typeof firstConfig !== 'object' || Array.isArray(firstConfig)) {
449
+ console.error(`${RED}⚠️ 错误: ${sourceLabel} 的 first 必须是对象(map),例如 {"shell":"init.sh"}${NC}`);
450
+ process.exit(1);
451
+ }
452
+ return firstConfig;
453
+ }
454
+
455
+ function addEnvTo(targetEnvs, env) {
440
456
  const parsed = parseEnvEntry(env);
441
- CONTAINER_ENVS.push("--env", `${parsed.key}=${parsed.value}`);
457
+ targetEnvs.push("--env", `${parsed.key}=${parsed.value}`);
442
458
  }
443
459
 
444
- function addEnvFile(envFile) {
460
+ function addEnv(env) {
461
+ addEnvTo(CONTAINER_ENVS, env);
462
+ }
463
+
464
+ function addEnvFileTo(targetEnvs, envFile) {
445
465
  const filePath = String(envFile || '').trim();
446
466
  if (!path.isAbsolute(filePath)) {
447
467
  console.error(`${RED}⚠️ 错误: --env-file 仅支持绝对路径: ${envFile}${NC}`);
@@ -472,7 +492,7 @@ function addEnvFile(envFile) {
472
492
  }
473
493
 
474
494
  if (key) {
475
- CONTAINER_ENVS.push("--env", `${key}=${value}`);
495
+ targetEnvs.push("--env", `${key}=${value}`);
476
496
  }
477
497
  }
478
498
  }
@@ -482,6 +502,10 @@ function addEnvFile(envFile) {
482
502
  return {};
483
503
  }
484
504
 
505
+ function addEnvFile(envFile) {
506
+ return addEnvFileTo(CONTAINER_ENVS, envFile);
507
+ }
508
+
485
509
  function addVolume(volume) {
486
510
  CONTAINER_VOLUMES.push("--volume", volume);
487
511
  }
@@ -1128,6 +1152,8 @@ async function setupCommander() {
1128
1152
 
1129
1153
  // Load run config if specified
1130
1154
  const runConfig = options.run ? loadRunConfig(options.run, config) : {};
1155
+ const globalFirstConfig = normalizeFirstConfig(config.first, '全局配置');
1156
+ const runFirstConfig = normalizeFirstConfig(runConfig.first, '运行配置');
1131
1157
 
1132
1158
  // Merge configs: command line > run config > global config > defaults
1133
1159
  // Override mode (scalar values): use first defined value
@@ -1158,6 +1184,18 @@ async function setupCommander() {
1158
1184
  if (mergedShellSuffix) {
1159
1185
  EXEC_COMMAND_SUFFIX = normalizeCommandSuffix(mergedShellSuffix);
1160
1186
  }
1187
+ const mergedFirstShellPrefix = pickConfigValue(runFirstConfig.shellPrefix, globalFirstConfig.shellPrefix);
1188
+ if (mergedFirstShellPrefix) {
1189
+ FIRST_EXEC_COMMAND_PREFIX = `${mergedFirstShellPrefix} `;
1190
+ }
1191
+ const mergedFirstShell = pickConfigValue(runFirstConfig.shell, globalFirstConfig.shell);
1192
+ if (mergedFirstShell) {
1193
+ FIRST_EXEC_COMMAND = mergedFirstShell;
1194
+ }
1195
+ const mergedFirstShellSuffix = pickConfigValue(runFirstConfig.shellSuffix, globalFirstConfig.shellSuffix);
1196
+ if (mergedFirstShellSuffix) {
1197
+ FIRST_EXEC_COMMAND_SUFFIX = normalizeCommandSuffix(mergedFirstShellSuffix);
1198
+ }
1161
1199
 
1162
1200
  // Basic name validation to reduce injection risk
1163
1201
  validateName('containerName', CONTAINER_NAME, SAFE_CONTAINER_NAME_PATTERN);
@@ -1181,6 +1219,18 @@ async function setupCommander() {
1181
1219
  };
1182
1220
  Object.entries(envMap).forEach(([key, value]) => addEnv(`${key}=${value}`));
1183
1221
 
1222
+ const firstEnvFileList = [
1223
+ ...toArray(globalFirstConfig.envFile),
1224
+ ...toArray(runFirstConfig.envFile)
1225
+ ].filter(Boolean);
1226
+ firstEnvFileList.forEach(ef => addEnvFileTo(FIRST_CONTAINER_ENVS, ef));
1227
+
1228
+ const firstEnvMap = {
1229
+ ...normalizeJsonEnvMap(globalFirstConfig.env, '全局配置 first'),
1230
+ ...normalizeJsonEnvMap(runFirstConfig.env, '运行配置 first')
1231
+ };
1232
+ Object.entries(firstEnvMap).forEach(([key, value]) => addEnvTo(FIRST_CONTAINER_ENVS, `${key}=${value}`));
1233
+
1184
1234
  const volumeList = mergeArrayConfig(config.volumes, runConfig.volumes, options.volume);
1185
1235
  volumeList.forEach(v => addVolume(v));
1186
1236
 
@@ -1267,6 +1317,18 @@ async function setupCommander() {
1267
1317
  prefix: EXEC_COMMAND_PREFIX,
1268
1318
  shell: EXEC_COMMAND,
1269
1319
  suffix: EXEC_COMMAND_SUFFIX
1320
+ },
1321
+ first: {
1322
+ envFile: firstEnvFileList,
1323
+ env: firstEnvMap,
1324
+ shellPrefix: FIRST_EXEC_COMMAND_PREFIX.trim(),
1325
+ shell: FIRST_EXEC_COMMAND || "",
1326
+ shellSuffix: FIRST_EXEC_COMMAND_SUFFIX || "",
1327
+ exec: {
1328
+ prefix: FIRST_EXEC_COMMAND_PREFIX,
1329
+ shell: FIRST_EXEC_COMMAND,
1330
+ suffix: FIRST_EXEC_COMMAND_SUFFIX
1331
+ }
1270
1332
  }
1271
1333
  };
1272
1334
  // 敏感信息脱敏
@@ -1300,8 +1362,12 @@ function createRuntimeContext(modeState = {}) {
1300
1362
  execCommand: EXEC_COMMAND,
1301
1363
  execCommandPrefix: EXEC_COMMAND_PREFIX,
1302
1364
  execCommandSuffix: EXEC_COMMAND_SUFFIX,
1365
+ firstExecCommand: FIRST_EXEC_COMMAND,
1366
+ firstExecCommandPrefix: FIRST_EXEC_COMMAND_PREFIX,
1367
+ firstExecCommandSuffix: FIRST_EXEC_COMMAND_SUFFIX,
1303
1368
  contModeArgs: CONT_MODE_ARGS,
1304
1369
  containerEnvs: CONTAINER_ENVS,
1370
+ firstContainerEnvs: FIRST_CONTAINER_ENVS,
1305
1371
  containerVolumes: CONTAINER_VOLUMES,
1306
1372
  containerPorts: CONTAINER_PORTS,
1307
1373
  quiet: QUIET,
@@ -1379,6 +1445,42 @@ function joinExecCommand(prefix, command, suffix) {
1379
1445
  return `${prefix || ''}${command || ''}${suffix || ''}`;
1380
1446
  }
1381
1447
 
1448
+ function executeFirstCommand(runtime) {
1449
+ if (!runtime.firstExecCommand || !String(runtime.firstExecCommand).trim()) {
1450
+ return;
1451
+ }
1452
+
1453
+ const firstCommand = joinExecCommand(
1454
+ runtime.firstExecCommandPrefix,
1455
+ runtime.firstExecCommand,
1456
+ runtime.firstExecCommandSuffix
1457
+ );
1458
+
1459
+ if (!(runtime.quiet.cmd || runtime.quiet.full)) {
1460
+ console.log(`${BLUE}----------------------------------------${NC}`);
1461
+ console.log(`⚙️ 首次预执行命令: ${YELLOW}${firstCommand}${NC}`);
1462
+ }
1463
+
1464
+ const firstExecArgs = [
1465
+ 'exec',
1466
+ ...(runtime.firstContainerEnvs || []),
1467
+ runtime.containerName,
1468
+ '/bin/bash',
1469
+ '-c',
1470
+ firstCommand
1471
+ ];
1472
+ const firstExecResult = spawnSync(`${DOCKER_CMD}`, firstExecArgs, { stdio: 'inherit' });
1473
+ if (firstExecResult.error) {
1474
+ throw firstExecResult.error;
1475
+ }
1476
+ if (typeof firstExecResult.status === 'number' && firstExecResult.status !== 0) {
1477
+ throw new Error(`首次预执行命令失败,退出码: ${firstExecResult.status}`);
1478
+ }
1479
+ if (firstExecResult.signal) {
1480
+ throw new Error(`首次预执行命令被信号终止: ${firstExecResult.signal}`);
1481
+ }
1482
+ }
1483
+
1382
1484
  /**
1383
1485
  * 创建新容器
1384
1486
  * @returns {Promise<string>} 默认命令
@@ -1412,6 +1514,9 @@ async function createNewContainer(runtime) {
1412
1514
  // Wait for container to be ready
1413
1515
  await waitForContainerReady(runtime.containerName);
1414
1516
 
1517
+ // Run one-time bootstrap command for newly created containers only.
1518
+ executeFirstCommand(runtime);
1519
+
1415
1520
  return defaultCommand;
1416
1521
  }
1417
1522
 
@@ -11,6 +11,15 @@
11
11
  "shellPrefix": "",
12
12
  "shell": "",
13
13
  "shellSuffix": "",
14
+ "agentPromptCommand": "",
15
+ // 仅首次创建容器时执行一次(创建后、常规 shell 前)
16
+ "first": {
17
+ "shellPrefix": "",
18
+ "shell": "",
19
+ "shellSuffix": "",
20
+ "env": {},
21
+ "envFile": []
22
+ },
14
23
  "yolo": "",
15
24
  "serverUser": "admin",
16
25
  "serverPass": "change-this-password",
@@ -58,6 +67,10 @@
58
67
  "containerName": "my-claude-{now}",
59
68
  "yolo": "c",
60
69
  "shell": "claude",
70
+ "agentPromptCommand": "claude -p {prompt}",
71
+ "first": {
72
+ "shell": "echo first-init"
73
+ },
61
74
  "env": {
62
75
  "ANTHROPIC_MODEL": "claude-sonnet-4-5"
63
76
  },
@@ -9,6 +9,13 @@ const AGENT_RESUME_ARG_MAP = {
9
9
  opencode: '-c'
10
10
  };
11
11
 
12
+ const AGENT_PROMPT_TEMPLATE_MAP = {
13
+ claude: 'claude -p {prompt}',
14
+ gemini: 'gemini -p {prompt}',
15
+ codex: 'codex exec {prompt}',
16
+ opencode: 'opencode run {prompt}'
17
+ };
18
+
12
19
  function stripLeadingAssignments(commandText) {
13
20
  let rest = String(commandText || '').trim();
14
21
  const assignmentPattern = /^(?:[A-Za-z_][A-Za-z0-9_]*=)(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s]+)(?:\s+|$)/;
@@ -67,6 +74,11 @@ function resolveAgentResumeArg(commandText) {
67
74
  return AGENT_RESUME_ARG_MAP[program] || '';
68
75
  }
69
76
 
77
+ function resolveAgentPromptCommandTemplate(commandText) {
78
+ const program = resolveAgentProgram(commandText);
79
+ return AGENT_PROMPT_TEMPLATE_MAP[program] || '';
80
+ }
81
+
70
82
  function buildAgentResumeCommand(commandText) {
71
83
  const baseCommand = String(commandText || '').trim();
72
84
  if (!baseCommand) {
@@ -80,6 +92,8 @@ function buildAgentResumeCommand(commandText) {
80
92
  }
81
93
 
82
94
  module.exports = {
95
+ resolveAgentProgram,
83
96
  resolveAgentResumeArg,
84
- buildAgentResumeCommand
97
+ buildAgentResumeCommand,
98
+ resolveAgentPromptCommandTemplate
85
99
  };
@@ -437,20 +437,24 @@ textarea:focus-visible {
437
437
  box-shadow: inset 3px 0 0 var(--accent), 0 0 0 2px rgba(196, 85, 31, 0.14);
438
438
  }
439
439
 
440
- .session-item.history-only {
441
- color: #b3ab9f;
442
- }
443
-
444
- .session-item.history-only .session-name,
445
- .session-item.history-only .session-count,
446
- .session-item.history-only .session-time {
447
- color: #b3ab9f;
440
+ .session-item.status-history:not(.active),
441
+ .session-item.status-stopped:not(.active),
442
+ .session-item.status-unknown:not(.active) {
443
+ border-color: rgba(181, 146, 99, 0.22);
444
+ background: rgba(255, 252, 247, 0.72);
445
+ box-shadow: none;
448
446
  }
449
447
 
450
- .session-item.history-only .session-status {
451
- background: #f7f3ed;
452
- border-color: #e3dbd1;
453
- color: #b3ab9f;
448
+ .session-item.status-history:not(.active) .session-name,
449
+ .session-item.status-history:not(.active) .session-meta,
450
+ .session-item.status-history:not(.active) .session-time,
451
+ .session-item.status-stopped:not(.active) .session-name,
452
+ .session-item.status-stopped:not(.active) .session-meta,
453
+ .session-item.status-stopped:not(.active) .session-time,
454
+ .session-item.status-unknown:not(.active) .session-name,
455
+ .session-item.status-unknown:not(.active) .session-meta,
456
+ .session-item.status-unknown:not(.active) .session-time {
457
+ opacity: 0.25;
454
458
  }
455
459
 
456
460
  .session-name {
@@ -641,6 +645,7 @@ textarea:focus-visible {
641
645
  }
642
646
 
643
647
  body.command-mode #modeCommandBtn,
648
+ body.agent-mode #modeAgentBtn,
644
649
  body.terminal-mode #modeTerminalBtn {
645
650
  color: #ffffff;
646
651
  background: var(--accent);
@@ -722,14 +727,26 @@ body.command-mode #messages {
722
727
  display: flex;
723
728
  }
724
729
 
730
+ body.agent-mode #messages {
731
+ display: flex;
732
+ }
733
+
725
734
  body.command-mode #terminalPanel {
726
735
  display: none;
727
736
  }
728
737
 
738
+ body.agent-mode #terminalPanel {
739
+ display: none;
740
+ }
741
+
729
742
  body.command-mode .composer {
730
743
  display: block;
731
744
  }
732
745
 
746
+ body.agent-mode .composer {
747
+ display: block;
748
+ }
749
+
733
750
  body.terminal-mode #messages {
734
751
  display: none;
735
752
  }
@@ -834,6 +851,23 @@ body.terminal-mode .composer {
834
851
  opacity: 0.78;
835
852
  }
836
853
 
854
+ body.agent-mode .msg.origin-command .bubble,
855
+ body.agent-mode .msg.origin-command .msg-meta,
856
+ body.agent-mode .msg.origin-command .msg-exit {
857
+ opacity: 0.25;
858
+ }
859
+
860
+ body.command-mode .msg.origin-agent .bubble,
861
+ body.command-mode .msg.origin-agent .msg-meta,
862
+ body.command-mode .msg.origin-agent .msg-exit {
863
+ opacity: 0.25;
864
+ }
865
+
866
+ body.agent-mode .msg.origin-command .bubble,
867
+ body.command-mode .msg.origin-agent .bubble {
868
+ box-shadow: none;
869
+ }
870
+
837
871
  .bubble pre {
838
872
  margin: 0;
839
873
  white-space: pre-wrap;
@@ -8,6 +8,7 @@
8
8
  />
9
9
  <title>MANYOYO Web</title>
10
10
  <link rel="stylesheet" href="/app/frontend/app.css" />
11
+ <link rel="stylesheet" href="/app/frontend/markdown.css" />
11
12
  <link rel="stylesheet" href="/app/vendor/xterm.css" />
12
13
  </head>
13
14
  <body>
@@ -65,7 +66,8 @@
65
66
  </header>
66
67
  <section class="mode-switch" id="modeSwitch">
67
68
  <div class="mode-switch-left">
68
- <button type="button" id="modeCommandBtn" class="secondary is-active">命令模式</button>
69
+ <button type="button" id="modeAgentBtn" class="secondary is-active">AGENT 模式</button>
70
+ <button type="button" id="modeCommandBtn" class="secondary">命令模式</button>
69
71
  <button type="button" id="modeTerminalBtn" class="secondary">交互终端</button>
70
72
  </div>
71
73
  <div class="mode-terminal-controls">
@@ -85,7 +87,7 @@
85
87
  <button type="submit" id="sendBtn">发送</button>
86
88
  </div>
87
89
  <div class="composer-foot">
88
- <span>Enter 发送 · Shift/Alt + Enter 换行</span>
90
+ <span id="composerHint">Enter 发送 · Shift/Alt + Enter 换行</span>
89
91
  <span id="sendState" class="send-state">未选择会话</span>
90
92
  </div>
91
93
  </form>
@@ -140,6 +142,7 @@
140
142
  <label>shell<input id="createShell" placeholder="例如 claude / codex" /></label>
141
143
  <label>shellSuffix<input id="createShellSuffix" placeholder="例如 --dangerously-skip-permissions" /></label>
142
144
  <label>yolo<input id="createYolo" placeholder="例如 c / cx / gm / oc" /></label>
145
+ <label>agentPromptCommand<input id="createAgentPromptCommand" placeholder="例如 codex exec --plain-text {prompt}" /></label>
143
146
  </div>
144
147
  <label class="text-block">env (KEY=VALUE,每行一项)
145
148
  <textarea id="createEnv" placeholder="KEY=value"></textarea>
@@ -162,6 +165,8 @@
162
165
 
163
166
  <script src="/app/vendor/xterm.js"></script>
164
167
  <script src="/app/vendor/xterm-addon-fit.js"></script>
168
+ <script src="/app/vendor/marked.min.js"></script>
169
+ <script src="/app/frontend/markdown-renderer.js"></script>
165
170
  <script src="/app/frontend/app.js"></script>
166
171
  </body>
167
172
  </html>