@xcanwin/manyoyo 5.1.10 → 5.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/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
  };
@@ -641,6 +641,7 @@ textarea:focus-visible {
641
641
  }
642
642
 
643
643
  body.command-mode #modeCommandBtn,
644
+ body.agent-mode #modeAgentBtn,
644
645
  body.terminal-mode #modeTerminalBtn {
645
646
  color: #ffffff;
646
647
  background: var(--accent);
@@ -722,14 +723,26 @@ body.command-mode #messages {
722
723
  display: flex;
723
724
  }
724
725
 
726
+ body.agent-mode #messages {
727
+ display: flex;
728
+ }
729
+
725
730
  body.command-mode #terminalPanel {
726
731
  display: none;
727
732
  }
728
733
 
734
+ body.agent-mode #terminalPanel {
735
+ display: none;
736
+ }
737
+
729
738
  body.command-mode .composer {
730
739
  display: block;
731
740
  }
732
741
 
742
+ body.agent-mode .composer {
743
+ display: block;
744
+ }
745
+
733
746
  body.terminal-mode #messages {
734
747
  display: none;
735
748
  }
@@ -66,6 +66,7 @@
66
66
  <section class="mode-switch" id="modeSwitch">
67
67
  <div class="mode-switch-left">
68
68
  <button type="button" id="modeCommandBtn" class="secondary is-active">命令模式</button>
69
+ <button type="button" id="modeAgentBtn" class="secondary">AGENT 模式</button>
69
70
  <button type="button" id="modeTerminalBtn" class="secondary">交互终端</button>
70
71
  </div>
71
72
  <div class="mode-terminal-controls">
@@ -85,7 +86,7 @@
85
86
  <button type="submit" id="sendBtn">发送</button>
86
87
  </div>
87
88
  <div class="composer-foot">
88
- <span>Enter 发送 · Shift/Alt + Enter 换行</span>
89
+ <span id="composerHint">Enter 发送 · Shift/Alt + Enter 换行</span>
89
90
  <span id="sendState" class="send-state">未选择会话</span>
90
91
  </div>
91
92
  </form>
@@ -140,6 +141,7 @@
140
141
  <label>shell<input id="createShell" placeholder="例如 claude / codex" /></label>
141
142
  <label>shellSuffix<input id="createShellSuffix" placeholder="例如 --dangerously-skip-permissions" /></label>
142
143
  <label>yolo<input id="createYolo" placeholder="例如 c / cx / gm / oc" /></label>
144
+ <label>agentPromptCommand<input id="createAgentPromptCommand" placeholder="例如 codex exec --plain-text {prompt}" /></label>
143
145
  </div>
144
146
  <label class="text-block">env (KEY=VALUE,每行一项)
145
147
  <textarea id="createEnv" placeholder="KEY=value"></textarea>
@@ -52,6 +52,7 @@
52
52
  configSaving: false,
53
53
  createLoading: false,
54
54
  createSubmitting: false,
55
+ createAgentPromptAuto: false,
55
56
  createDefaults: null,
56
57
  createRuns: {},
57
58
  sessionNodeMap: new Map(),
@@ -105,6 +106,7 @@
105
106
  const createShellPrefix = document.getElementById('createShellPrefix');
106
107
  const createShell = document.getElementById('createShell');
107
108
  const createShellSuffix = document.getElementById('createShellSuffix');
109
+ const createAgentPromptCommand = document.getElementById('createAgentPromptCommand');
108
110
  const createYolo = document.getElementById('createYolo');
109
111
  const createEnv = document.getElementById('createEnv');
110
112
  const createEnvFile = document.getElementById('createEnvFile');
@@ -112,6 +114,7 @@
112
114
  const activeTitle = document.getElementById('activeTitle');
113
115
  const activeMeta = document.getElementById('activeMeta');
114
116
  const modeCommandBtn = document.getElementById('modeCommandBtn');
117
+ const modeAgentBtn = document.getElementById('modeAgentBtn');
115
118
  const modeTerminalBtn = document.getElementById('modeTerminalBtn');
116
119
  const messagesNode = document.getElementById('messages');
117
120
  const terminalPanel = document.getElementById('terminalPanel');
@@ -121,6 +124,7 @@
121
124
  const terminalScreen = document.getElementById('terminalScreen');
122
125
  const composer = document.getElementById('composer');
123
126
  const commandInput = document.getElementById('commandInput');
127
+ const composerHint = document.getElementById('composerHint');
124
128
  const sendState = document.getElementById('sendState');
125
129
  const sendBtn = document.getElementById('sendBtn');
126
130
  const refreshBtn = document.getElementById('refreshBtn');
@@ -133,10 +137,33 @@
133
137
  const TERMINAL_MIN_ROWS = 12;
134
138
  const TERMINAL_DEFAULT_COLS = 120;
135
139
  const TERMINAL_DEFAULT_ROWS = 36;
140
+ const YOLO_COMMAND_MAP = {
141
+ claude: 'IS_SANDBOX=1 claude --dangerously-skip-permissions',
142
+ cc: 'IS_SANDBOX=1 claude --dangerously-skip-permissions',
143
+ c: 'IS_SANDBOX=1 claude --dangerously-skip-permissions',
144
+ gemini: 'gemini --yolo',
145
+ gm: 'gemini --yolo',
146
+ g: 'gemini --yolo',
147
+ codex: 'codex --dangerously-bypass-approvals-and-sandbox',
148
+ cx: 'codex --dangerously-bypass-approvals-and-sandbox',
149
+ opencode: 'OPENCODE_PERMISSION=\'{"*":"allow"}\' opencode',
150
+ oc: 'OPENCODE_PERMISSION=\'{"*":"allow"}\' opencode'
151
+ };
152
+ const AGENT_PROMPT_TEMPLATE_MAP = {
153
+ claude: 'claude -p {prompt}',
154
+ gemini: 'gemini -p {prompt}',
155
+ codex: 'codex exec {prompt}',
156
+ opencode: 'opencode run {prompt}'
157
+ };
136
158
 
137
- function roleName(role) {
159
+ function roleName(role, message) {
138
160
  if (role === 'user') return '你';
139
- if (role === 'assistant') return '容器输出';
161
+ if (role === 'assistant') {
162
+ if (message && message.mode === 'agent') {
163
+ return 'AGENT 回复';
164
+ }
165
+ return '容器输出';
166
+ }
140
167
  return '系统';
141
168
  }
142
169
 
@@ -242,6 +269,97 @@
242
269
  return envMap;
243
270
  }
244
271
 
272
+ function buildDefaultCommand(shellPrefix, shell, shellSuffix) {
273
+ const parts = [];
274
+ if (shellPrefix && String(shellPrefix).trim()) {
275
+ parts.push(String(shellPrefix).trim());
276
+ }
277
+ if (shell && String(shell).trim()) {
278
+ parts.push(String(shell).trim());
279
+ }
280
+ if (shellSuffix && String(shellSuffix).trim()) {
281
+ parts.push(String(shellSuffix).trim());
282
+ }
283
+ return parts.join(' ').trim();
284
+ }
285
+
286
+ function resolveYoloCommand(yolo) {
287
+ const key = String(yolo || '').trim().toLowerCase();
288
+ if (!key) return '';
289
+ return YOLO_COMMAND_MAP[key] || '';
290
+ }
291
+
292
+ function stripLeadingAssignments(commandText) {
293
+ let rest = String(commandText || '').trim();
294
+ const assignmentPattern = /^(?:[A-Za-z_][A-Za-z0-9_]*=)(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s]+)(?:\s+|$)/;
295
+ while (rest) {
296
+ const matched = rest.match(assignmentPattern);
297
+ if (!matched) break;
298
+ rest = rest.slice(matched[0].length).trim();
299
+ }
300
+ return rest;
301
+ }
302
+
303
+ function readLeadingToken(commandText) {
304
+ const text = String(commandText || '').trim();
305
+ if (!text) return { token: '', rest: '' };
306
+ const tokenMatch = text.match(/^(?:"((?:\\.|[^"])*)"|'((?:\\.|[^'])*)'|([^\s]+))(?:\s+|$)/);
307
+ if (!tokenMatch) return { token: '', rest: '' };
308
+ return {
309
+ token: tokenMatch[1] || tokenMatch[2] || tokenMatch[3] || '',
310
+ rest: text.slice(tokenMatch[0].length).trim()
311
+ };
312
+ }
313
+
314
+ function normalizeProgramName(token) {
315
+ const text = String(token || '').trim();
316
+ if (!text) return '';
317
+ return text.replace(/\\/g, '/').split('/').pop().toLowerCase();
318
+ }
319
+
320
+ function resolveAgentProgram(commandText) {
321
+ let rest = stripLeadingAssignments(commandText);
322
+ let leading = readLeadingToken(rest);
323
+ let program = normalizeProgramName(leading.token);
324
+ if (program === 'env') {
325
+ rest = stripLeadingAssignments(leading.rest);
326
+ leading = readLeadingToken(rest);
327
+ program = normalizeProgramName(leading.token);
328
+ }
329
+ return program;
330
+ }
331
+
332
+ function resolveAgentPromptTemplate(commandText) {
333
+ const program = resolveAgentProgram(commandText);
334
+ return AGENT_PROMPT_TEMPLATE_MAP[program] || '';
335
+ }
336
+
337
+ function inferCreateAgentPromptCommand() {
338
+ let shell = (createShell.value || '').trim();
339
+ const yoloCommand = resolveYoloCommand(createYolo.value || '');
340
+ if (yoloCommand) {
341
+ shell = yoloCommand;
342
+ }
343
+ const fullCommand = buildDefaultCommand(
344
+ (createShellPrefix.value || '').trim(),
345
+ shell,
346
+ (createShellSuffix.value || '').trim()
347
+ );
348
+ return resolveAgentPromptTemplate(fullCommand);
349
+ }
350
+
351
+ function updateCreateAgentPromptCommandFromCommand() {
352
+ if (!createAgentPromptCommand) return;
353
+ const current = String(createAgentPromptCommand.value || '').trim();
354
+ const inferred = inferCreateAgentPromptCommand();
355
+ const canAutoReplace = state.createAgentPromptAuto || !current;
356
+ if (!canAutoReplace) {
357
+ return;
358
+ }
359
+ createAgentPromptCommand.value = inferred;
360
+ state.createAgentPromptAuto = Boolean(inferred);
361
+ }
362
+
245
363
  function fillCreateForm(defaults) {
246
364
  const value = defaults && typeof defaults === 'object' ? defaults : {};
247
365
  createContainerName.value = value.containerName || '';
@@ -253,10 +371,13 @@
253
371
  createShellPrefix.value = value.shellPrefix || '';
254
372
  createShell.value = value.shell || '';
255
373
  createShellSuffix.value = value.shellSuffix || '';
374
+ createAgentPromptCommand.value = value.agentPromptCommand || '';
375
+ state.createAgentPromptAuto = false;
256
376
  createYolo.value = value.yolo || '';
257
377
  createEnv.value = envMapToText(value.env);
258
378
  createEnvFile.value = Array.isArray(value.envFile) ? value.envFile.join('\n') : '';
259
379
  createVolumes.value = Array.isArray(value.volumes) ? value.volumes.join('\n') : '';
380
+ updateCreateAgentPromptCommandFromCommand();
260
381
  }
261
382
 
262
383
  function mergeCreateDefaults(baseDefaults, runConfig) {
@@ -272,6 +393,7 @@
272
393
  shellPrefix: run.shellPrefix != null ? String(run.shellPrefix) : (base.shellPrefix || ''),
273
394
  shell: run.shell != null ? String(run.shell) : (base.shell || ''),
274
395
  shellSuffix: run.shellSuffix != null ? String(run.shellSuffix) : (base.shellSuffix || ''),
396
+ agentPromptCommand: run.agentPromptCommand != null ? String(run.agentPromptCommand) : (base.agentPromptCommand || ''),
275
397
  yolo: run.yolo != null ? String(run.yolo) : (base.yolo || ''),
276
398
  env: {},
277
399
  envFile: [],
@@ -358,6 +480,7 @@
358
480
  shellPrefix: (createShellPrefix.value || '').trim(),
359
481
  shell: (createShell.value || '').trim(),
360
482
  shellSuffix: (createShellSuffix.value || '').trim(),
483
+ agentPromptCommand: (createAgentPromptCommand.value || '').trim(),
361
484
  yolo: (createYolo.value || '').trim(),
362
485
  env: textToEnvMap(createEnv.value),
363
486
  envFile: textToLineArray(createEnvFile.value),
@@ -392,6 +515,15 @@
392
515
  }) || null;
393
516
  }
394
517
 
518
+ function isComposerMode() {
519
+ return state.mode === 'command' || state.mode === 'agent';
520
+ }
521
+
522
+ function isActiveAgentEnabled() {
523
+ const active = getActiveSession();
524
+ return Boolean(active && active.agentEnabled);
525
+ }
526
+
395
527
  function buildActiveMeta(session) {
396
528
  if (!session) {
397
529
  return '会话不可用';
@@ -411,7 +543,7 @@
411
543
 
412
544
  lines.push({
413
545
  className: 'msg-meta-role',
414
- text: roleName(message && message.role)
546
+ text: roleName(message && message.role, message)
415
547
  });
416
548
 
417
549
  return lines;
@@ -761,7 +893,7 @@
761
893
  if (!state.active) {
762
894
  activeTitle.textContent = '未选择会话';
763
895
  activeMeta.textContent = '请选择左侧会话';
764
- if (state.mode === 'command') {
896
+ if (isComposerMode()) {
765
897
  commandInput.value = '';
766
898
  }
767
899
  } else {
@@ -769,21 +901,31 @@
769
901
  activeMeta.textContent = buildActiveMeta(getActiveSession());
770
902
  }
771
903
 
772
- const commandMode = state.mode !== 'terminal';
904
+ const commandMode = state.mode === 'command';
905
+ const agentMode = state.mode === 'agent';
906
+ const terminalMode = state.mode === 'terminal';
907
+ const composerMode = commandMode || agentMode;
908
+ const agentEnabled = isActiveAgentEnabled();
909
+
773
910
  document.body.classList.toggle('command-mode', commandMode);
774
- document.body.classList.toggle('terminal-mode', !commandMode);
911
+ document.body.classList.toggle('agent-mode', agentMode);
912
+ document.body.classList.toggle('terminal-mode', terminalMode);
775
913
  if (modeCommandBtn) {
776
914
  modeCommandBtn.classList.toggle('is-active', commandMode);
777
915
  modeCommandBtn.setAttribute('aria-pressed', commandMode ? 'true' : 'false');
778
916
  }
917
+ if (modeAgentBtn) {
918
+ modeAgentBtn.classList.toggle('is-active', agentMode);
919
+ modeAgentBtn.setAttribute('aria-pressed', agentMode ? 'true' : 'false');
920
+ }
779
921
  if (modeTerminalBtn) {
780
- modeTerminalBtn.classList.toggle('is-active', !commandMode);
781
- modeTerminalBtn.setAttribute('aria-pressed', !commandMode ? 'true' : 'false');
922
+ modeTerminalBtn.classList.toggle('is-active', terminalMode);
923
+ modeTerminalBtn.setAttribute('aria-pressed', terminalMode ? 'true' : 'false');
782
924
  }
783
925
  if (terminalPanel) {
784
- terminalPanel.hidden = commandMode;
926
+ terminalPanel.hidden = !terminalMode;
785
927
  }
786
- if (!commandMode && state.terminal.terminalReady) {
928
+ if (terminalMode && state.terminal.terminalReady) {
787
929
  scheduleTerminalFit(false);
788
930
  }
789
931
 
@@ -791,8 +933,18 @@
791
933
  refreshBtn.disabled = busy;
792
934
  removeBtn.disabled = !state.active || busy;
793
935
  removeAllBtn.disabled = !state.active || busy;
794
- sendBtn.disabled = !commandMode || !state.active || busy;
795
- commandInput.disabled = !commandMode || !state.active || state.sending;
936
+ sendBtn.disabled = !composerMode || !state.active || busy || (agentMode && !agentEnabled);
937
+ commandInput.disabled = !composerMode || !state.active || state.sending || (agentMode && !agentEnabled);
938
+ if (commandInput) {
939
+ commandInput.placeholder = agentMode
940
+ ? '输入提示词,例如:请帮我分析当前项目结构并给出重构建议'
941
+ : '输入容器命令,例如: ls -la';
942
+ }
943
+ if (composerHint) {
944
+ composerHint.textContent = agentMode
945
+ ? 'Enter 发送提示词 · Shift/Alt + Enter 换行'
946
+ : 'Enter 发送 · Shift/Alt + Enter 换行';
947
+ }
796
948
  if (openCreateBtn) {
797
949
  openCreateBtn.disabled = state.createLoading || state.createSubmitting;
798
950
  }
@@ -835,6 +987,8 @@
835
987
 
836
988
  if (!state.active) {
837
989
  sendState.textContent = '未选择会话';
990
+ } else if (agentMode && !agentEnabled) {
991
+ sendState.textContent = '当前会话未配置 AGENT 模板';
838
992
  } else if (state.sending) {
839
993
  sendState.textContent = '发送中...';
840
994
  } else if (state.loadingSessions || state.loadingMessages) {
@@ -1228,7 +1382,9 @@
1228
1382
  messagesNode.innerHTML = '';
1229
1383
  const empty = document.createElement('div');
1230
1384
  empty.className = 'empty';
1231
- empty.textContent = '输入命令后,容器输出会显示在这里。';
1385
+ empty.textContent = state.mode === 'agent'
1386
+ ? '输入提示词后,AGENT 回复会显示在这里。'
1387
+ : '输入命令后,容器输出会显示在这里。';
1232
1388
  messagesNode.appendChild(empty);
1233
1389
  state.messageRenderKeys = [];
1234
1390
  return;
@@ -1442,7 +1598,7 @@
1442
1598
  return -1;
1443
1599
  }
1444
1600
 
1445
- function appendAssistantMessageLocal(sessionName, result) {
1601
+ function appendAssistantMessageLocal(sessionName, result, mode) {
1446
1602
  if (state.active !== sessionName) {
1447
1603
  return;
1448
1604
  }
@@ -1453,7 +1609,8 @@
1453
1609
  role: 'assistant',
1454
1610
  content: outputText,
1455
1611
  timestamp: new Date().toISOString(),
1456
- exitCode: exitCode
1612
+ exitCode: exitCode,
1613
+ mode: mode || 'command'
1457
1614
  });
1458
1615
  }
1459
1616
 
@@ -1508,6 +1665,26 @@
1508
1665
  });
1509
1666
  }
1510
1667
 
1668
+ [createShellPrefix, createShell, createShellSuffix, createYolo].forEach(function (inputNode) {
1669
+ if (!inputNode) return;
1670
+ inputNode.addEventListener('input', function () {
1671
+ updateCreateAgentPromptCommandFromCommand();
1672
+ });
1673
+ });
1674
+
1675
+ if (createAgentPromptCommand) {
1676
+ createAgentPromptCommand.addEventListener('input', function () {
1677
+ const current = String(createAgentPromptCommand.value || '').trim();
1678
+ if (!current) {
1679
+ state.createAgentPromptAuto = true;
1680
+ updateCreateAgentPromptCommandFromCommand();
1681
+ return;
1682
+ }
1683
+ const inferred = inferCreateAgentPromptCommand();
1684
+ state.createAgentPromptAuto = Boolean(inferred) && inferred === current;
1685
+ });
1686
+ }
1687
+
1511
1688
  if (createForm) {
1512
1689
  createForm.addEventListener('submit', async function (event) {
1513
1690
  event.preventDefault();
@@ -1517,9 +1694,13 @@
1517
1694
  syncUi();
1518
1695
  try {
1519
1696
  const createOptions = collectCreateOptions();
1697
+ const runName = createRun ? String(createRun.value || '').trim() : '';
1520
1698
  const data = await api('/api/sessions', {
1521
1699
  method: 'POST',
1522
- body: JSON.stringify({ createOptions: createOptions })
1700
+ body: JSON.stringify({
1701
+ run: runName || undefined,
1702
+ createOptions: createOptions
1703
+ })
1523
1704
  });
1524
1705
  closeCreateModal();
1525
1706
  await loadSessions(data.name);
@@ -1540,16 +1721,23 @@
1540
1721
  if (!state.active) return;
1541
1722
  if (state.sending) return;
1542
1723
  if (state.loadingSessions || state.loadingMessages) return;
1543
- const command = (commandInput.value || '').trim();
1544
- if (!command) return;
1724
+ if (!isComposerMode()) return;
1725
+ const mode = state.mode === 'agent' ? 'agent' : 'command';
1726
+ if (mode === 'agent' && !isActiveAgentEnabled()) {
1727
+ syncUi();
1728
+ return;
1729
+ }
1730
+ const inputText = (commandInput.value || '').trim();
1731
+ if (!inputText) return;
1545
1732
 
1546
1733
  const submitSession = state.active;
1547
1734
  const pendingMessage = {
1548
1735
  id: createLocalMessageId('local-user'),
1549
1736
  role: 'user',
1550
- content: command,
1737
+ content: inputText,
1551
1738
  timestamp: new Date().toISOString(),
1552
- pending: true
1739
+ pending: true,
1740
+ mode: mode
1553
1741
  };
1554
1742
  state.messages.push(pendingMessage);
1555
1743
  renderMessages(state.messages, { stickToBottom: true });
@@ -1559,9 +1747,14 @@
1559
1747
  try {
1560
1748
  commandInput.value = '';
1561
1749
  commandInput.focus();
1562
- const runResult = await api('/api/sessions/' + encodeURIComponent(submitSession) + '/run', {
1750
+ const endpoint = mode === 'agent' ? '/agent' : '/run';
1751
+ const runResult = await api('/api/sessions/' + encodeURIComponent(submitSession) + endpoint, {
1563
1752
  method: 'POST',
1564
- body: JSON.stringify({ command: command })
1753
+ body: JSON.stringify(
1754
+ mode === 'agent'
1755
+ ? { prompt: inputText }
1756
+ : { command: inputText }
1757
+ )
1565
1758
  });
1566
1759
  const pendingIndex = confirmPendingUserMessage(submitSession, pendingMessage.id);
1567
1760
  if (pendingIndex >= 0 && pendingIndex < state.messageRenderKeys.length) {
@@ -1572,7 +1765,7 @@
1572
1765
  }
1573
1766
  }
1574
1767
  }
1575
- appendAssistantMessageLocal(submitSession, runResult);
1768
+ appendAssistantMessageLocal(submitSession, runResult, mode);
1576
1769
  if (state.active === submitSession) {
1577
1770
  renderMessages(state.messages, { stickToBottom: true });
1578
1771
  }
@@ -1607,7 +1800,7 @@
1607
1800
 
1608
1801
  // Enter / Ctrl+Enter: 发送
1609
1802
  event.preventDefault();
1610
- if (!state.active || state.sending) {
1803
+ if (!state.active || state.sending || !isComposerMode()) {
1611
1804
  return;
1612
1805
  }
1613
1806
  composer.requestSubmit();
@@ -1616,6 +1809,16 @@
1616
1809
  if (modeCommandBtn) {
1617
1810
  modeCommandBtn.addEventListener('click', function () {
1618
1811
  state.mode = 'command';
1812
+ renderMessages(state.messages, { forceFullRender: true });
1813
+ syncUi();
1814
+ commandInput.focus();
1815
+ });
1816
+ }
1817
+
1818
+ if (modeAgentBtn) {
1819
+ modeAgentBtn.addEventListener('click', function () {
1820
+ state.mode = 'agent';
1821
+ renderMessages(state.messages, { forceFullRender: true });
1619
1822
  syncUi();
1620
1823
  commandInput.focus();
1621
1824
  });
@@ -1624,6 +1827,7 @@
1624
1827
  if (modeTerminalBtn) {
1625
1828
  modeTerminalBtn.addEventListener('click', function () {
1626
1829
  state.mode = 'terminal';
1830
+ renderMessages(state.messages, { forceFullRender: true });
1627
1831
  syncUi();
1628
1832
  if (ensureTerminalReady()) {
1629
1833
  if (!state.terminal.connected && !state.terminal.connecting) {
package/lib/web/server.js CHANGED
@@ -9,6 +9,7 @@ const http = require('http');
9
9
  const WebSocket = require('ws');
10
10
  const JSON5 = require('json5');
11
11
  const { buildContainerRunArgs } = require('../container-run');
12
+ const { resolveAgentPromptCommandTemplate } = require('../agent-resume');
12
13
 
13
14
  const WEB_HISTORY_MAX_MESSAGES = 500;
14
15
  const WEB_OUTPUT_MAX_CHARS = 16000;
@@ -48,6 +49,7 @@ const DEFAULT_WEB_CONFIG_TEMPLATE = `{
48
49
  "shellPrefix": "",
49
50
  "shell": "",
50
51
  "shellSuffix": "",
52
+ "agentPromptCommand": "",
51
53
  "yolo": "",
52
54
  "env": {},
53
55
  "envFile": [],
@@ -100,7 +102,12 @@ function loadWebSessionHistory(webHistoryDir, containerName) {
100
102
  ensureWebHistoryDir(webHistoryDir);
101
103
  const filePath = getWebHistoryFile(webHistoryDir, containerName);
102
104
  if (!fs.existsSync(filePath)) {
103
- return { containerName, updatedAt: null, messages: [] };
105
+ return {
106
+ containerName,
107
+ updatedAt: null,
108
+ messages: [],
109
+ agentPromptCommand: ''
110
+ };
104
111
  }
105
112
 
106
113
  try {
@@ -108,10 +115,18 @@ function loadWebSessionHistory(webHistoryDir, containerName) {
108
115
  return {
109
116
  containerName,
110
117
  updatedAt: data.updatedAt || null,
111
- messages: Array.isArray(data.messages) ? data.messages : []
118
+ messages: Array.isArray(data.messages) ? data.messages : [],
119
+ agentPromptCommand: typeof data.agentPromptCommand === 'string'
120
+ ? data.agentPromptCommand
121
+ : ''
112
122
  };
113
123
  } catch (e) {
114
- return { containerName, updatedAt: null, messages: [] };
124
+ return {
125
+ containerName,
126
+ updatedAt: null,
127
+ messages: [],
128
+ agentPromptCommand: ''
129
+ };
115
130
  }
116
131
  }
117
132
 
@@ -156,6 +171,12 @@ function appendWebSessionMessage(webHistoryDir, containerName, role, content, ex
156
171
  saveWebSessionHistory(webHistoryDir, containerName, history);
157
172
  }
158
173
 
174
+ function setWebSessionAgentPromptCommand(webHistoryDir, containerName, agentPromptCommand) {
175
+ const history = loadWebSessionHistory(webHistoryDir, containerName);
176
+ history.agentPromptCommand = normalizeAgentPromptCommandTemplate(agentPromptCommand, 'agentPromptCommand');
177
+ saveWebSessionHistory(webHistoryDir, containerName, history);
178
+ }
179
+
159
180
  function stripAnsi(text) {
160
181
  if (typeof text !== 'string') return '';
161
182
  return text.replace(/\x1b\[[0-9;]*m/g, '');
@@ -167,6 +188,38 @@ function clipText(text, maxChars = WEB_OUTPUT_MAX_CHARS) {
167
188
  return `${text.slice(0, maxChars)}\n...[truncated]`;
168
189
  }
169
190
 
191
+ function normalizeAgentPromptCommandTemplate(value, sourceLabel = 'agentPromptCommand') {
192
+ if (value === undefined || value === null) {
193
+ return '';
194
+ }
195
+ if (typeof value !== 'string') {
196
+ throw new Error(`${sourceLabel} 必须是字符串`);
197
+ }
198
+ const text = value.trim();
199
+ if (!text) {
200
+ return '';
201
+ }
202
+ if (!text.includes('{prompt}')) {
203
+ throw new Error(`${sourceLabel} 必须包含 {prompt} 占位符`);
204
+ }
205
+ return text;
206
+ }
207
+
208
+ function isAgentPromptCommandEnabled(value) {
209
+ return typeof value === 'string' && value.includes('{prompt}') && Boolean(value.trim());
210
+ }
211
+
212
+ function quoteBashSingleValue(value) {
213
+ const text = String(value || '');
214
+ return `'${text.replace(/'/g, `'\"'\"'`)}'`;
215
+ }
216
+
217
+ function renderAgentPromptCommand(template, prompt) {
218
+ const templateText = normalizeAgentPromptCommandTemplate(template, 'agentPromptCommand');
219
+ const safePrompt = quoteBashSingleValue(prompt);
220
+ return templateText.replace(/\{prompt\}/g, safePrompt);
221
+ }
222
+
170
223
  function secureStringEqual(a, b) {
171
224
  const aStr = String(a || '');
172
225
  const bStr = String(b || '');
@@ -478,11 +531,20 @@ function validateWebConfigShape(configObject) {
478
531
  if (hasOwn(config, 'ports')) {
479
532
  normalizeStringArray(config.ports, 'ports');
480
533
  }
534
+ if (hasOwn(config, 'agentPromptCommand')) {
535
+ normalizeAgentPromptCommandTemplate(config.agentPromptCommand, 'agentPromptCommand');
536
+ }
481
537
  if (hasOwn(config, 'runs')) {
482
538
  const runs = config.runs;
483
539
  if (runs !== undefined && (typeof runs !== 'object' || runs === null || Array.isArray(runs))) {
484
540
  throw new Error('runs 必须是对象(map)');
485
541
  }
542
+ Object.entries(toPlainObject(runs)).forEach(([runName, runConfig]) => {
543
+ const normalizedRun = toPlainObject(runConfig);
544
+ if (hasOwn(normalizedRun, 'agentPromptCommand')) {
545
+ normalizeAgentPromptCommandTemplate(normalizedRun.agentPromptCommand, `runs.${runName}.agentPromptCommand`);
546
+ }
547
+ });
486
548
  }
487
549
  }
488
550
 
@@ -556,6 +618,7 @@ function buildConfigDefaults(ctx, config) {
556
618
  shellPrefix: hasOwn(parsed, 'shellPrefix') ? String(parsed.shellPrefix || '') : '',
557
619
  shell: hasOwn(parsed, 'shell') ? String(parsed.shell || '') : '',
558
620
  shellSuffix: hasOwn(parsed, 'shellSuffix') ? String(parsed.shellSuffix || '') : '',
621
+ agentPromptCommand: '',
559
622
  yolo: hasOwn(parsed, 'yolo') ? String(parsed.yolo || '') : '',
560
623
  env: {},
561
624
  envFile: [],
@@ -583,6 +646,11 @@ function buildConfigDefaults(ctx, config) {
583
646
  } catch (e) {
584
647
  defaults.ports = [];
585
648
  }
649
+ try {
650
+ defaults.agentPromptCommand = normalizeAgentPromptCommandTemplate(parsed.agentPromptCommand, 'agentPromptCommand');
651
+ } catch (e) {
652
+ defaults.agentPromptCommand = '';
653
+ }
586
654
 
587
655
  return defaults;
588
656
  }
@@ -607,6 +675,9 @@ function buildCreateRuntime(ctx, state, payload) {
607
675
  const requestOptions = toPlainObject(body.createOptions);
608
676
  const snapshot = readWebConfigSnapshot(state.webConfigPath);
609
677
  const config = snapshot.parseError ? {} : snapshot.parsed;
678
+ const runs = toPlainObject(config.runs);
679
+ const runName = pickFirstString(body.run);
680
+ const runConfig = runName && hasOwn(runs, runName) ? toPlainObject(runs[runName]) : {};
610
681
 
611
682
  const hasRequestEnv = hasOwn(requestOptions, 'env');
612
683
  const hasRequestEnvFile = hasOwn(requestOptions, 'envFile');
@@ -663,6 +734,18 @@ function buildCreateRuntime(ctx, state, payload) {
663
734
  shell = yoloCommand;
664
735
  }
665
736
 
737
+ const configuredAgentPromptCommand = normalizeAgentPromptCommandTemplate(
738
+ hasOwn(requestOptions, 'agentPromptCommand')
739
+ ? requestOptions.agentPromptCommand
740
+ : (hasOwn(runConfig, 'agentPromptCommand') ? runConfig.agentPromptCommand : config.agentPromptCommand),
741
+ 'agentPromptCommand'
742
+ );
743
+ const inferredAgentPromptCommand = normalizeAgentPromptCommandTemplate(
744
+ resolveAgentPromptCommandTemplate(buildDefaultCommand(shellPrefix, shell, shellSuffix)),
745
+ 'agentPromptCommand'
746
+ );
747
+ const agentPromptCommand = configuredAgentPromptCommand || inferredAgentPromptCommand;
748
+
666
749
  let containerEnvs = Array.isArray(ctx.containerEnvs) ? ctx.containerEnvs.slice() : [];
667
750
  if (hasRequestEnv || hasRequestEnvFile || hasConfigEnv || hasConfigEnvFile) {
668
751
  const configEnv = normalizeEnvMap(config.env, 'config.env');
@@ -717,6 +800,7 @@ function buildCreateRuntime(ctx, state, payload) {
717
800
  containerEnvs,
718
801
  containerVolumes,
719
802
  containerPorts,
803
+ agentPromptCommand,
720
804
  defaultCommand: buildDefaultCommand(shellPrefix, shell, shellSuffix) || '/bin/bash',
721
805
  applied: {
722
806
  containerName,
@@ -728,6 +812,7 @@ function buildCreateRuntime(ctx, state, payload) {
728
812
  shellPrefix: shellPrefix || '',
729
813
  shell: shell || '',
730
814
  shellSuffix: shellSuffix || '',
815
+ agentEnabled: isAgentPromptCommandEnabled(agentPromptCommand),
731
816
  yolo: yolo || '',
732
817
  envCount: Math.floor(containerEnvs.length / 2),
733
818
  volumeCount: Math.floor(containerVolumes.length / 2),
@@ -918,7 +1003,8 @@ function buildSessionSummary(ctx, state, containerMap, name) {
918
1003
  status: containerInfo.status || 'history',
919
1004
  image: containerInfo.image || '',
920
1005
  updatedAt,
921
- messageCount: history.messages.length
1006
+ messageCount: history.messages.length,
1007
+ agentEnabled: isAgentPromptCommandEnabled(history.agentPromptCommand)
922
1008
  };
923
1009
  }
924
1010
 
@@ -1325,6 +1411,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
1325
1411
  }
1326
1412
 
1327
1413
  await ensureWebContainer(ctx, state, runtime);
1414
+ setWebSessionAgentPromptCommand(state.webHistoryDir, runtime.containerName, runtime.agentPromptCommand);
1328
1415
  sendJson(res, 200, { name: runtime.containerName, applied: runtime.applied });
1329
1416
  }
1330
1417
  },
@@ -1369,6 +1456,42 @@ async function handleWebApi(req, res, pathname, ctx, state) {
1369
1456
  sendJson(res, 200, { exitCode: result.exitCode, output: result.output });
1370
1457
  }
1371
1458
  },
1459
+ {
1460
+ method: 'POST',
1461
+ match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/agent$/),
1462
+ handler: async match => {
1463
+ const containerName = getValidSessionName(ctx, res, match[1]);
1464
+ if (!containerName) {
1465
+ return;
1466
+ }
1467
+
1468
+ const payload = await readJsonBody(req);
1469
+ const prompt = (payload.prompt || '').trim();
1470
+ if (!prompt) {
1471
+ sendJson(res, 400, { error: 'prompt 不能为空' });
1472
+ return;
1473
+ }
1474
+
1475
+ const history = loadWebSessionHistory(state.webHistoryDir, containerName);
1476
+ if (!isAgentPromptCommandEnabled(history.agentPromptCommand)) {
1477
+ sendJson(res, 400, { error: '当前会话未配置 agentPromptCommand' });
1478
+ return;
1479
+ }
1480
+
1481
+ const command = renderAgentPromptCommand(history.agentPromptCommand, prompt);
1482
+ await ensureWebContainer(ctx, state, containerName);
1483
+ appendWebSessionMessage(state.webHistoryDir, containerName, 'user', prompt, { mode: 'agent' });
1484
+ const result = await execCommandInWebContainer(ctx, containerName, command);
1485
+ appendWebSessionMessage(
1486
+ state.webHistoryDir,
1487
+ containerName,
1488
+ 'assistant',
1489
+ result.output,
1490
+ { exitCode: result.exitCode, mode: 'agent' }
1491
+ );
1492
+ sendJson(res, 200, { exitCode: result.exitCode, output: result.output });
1493
+ }
1494
+ },
1372
1495
  {
1373
1496
  method: 'POST',
1374
1497
  match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/remove$/),
@@ -1613,7 +1736,7 @@ async function startWebServer(options) {
1613
1736
  const { GREEN, CYAN, YELLOW, NC } = ctx.colors;
1614
1737
  const listenHost = formatUrlHost(ctx.serverHost);
1615
1738
  console.log(`${GREEN}✅ MANYOYO Web 服务已启动: http://${listenHost}:${listenPort}${NC}`);
1616
- console.log(`${CYAN}提示: 左侧是 manyoyo 容器会话列表,右侧支持命令模式与交互式终端模式。${NC}`);
1739
+ console.log(`${CYAN}提示: 左侧是 manyoyo 容器会话列表,右侧支持命令模式、AGENT 模式与交互式终端模式。${NC}`);
1617
1740
  if (ctx.serverHost === '0.0.0.0') {
1618
1741
  console.log(`${CYAN}提示: 当前监听全部网卡,请用本机局域网 IP 访问。${NC}`);
1619
1742
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcanwin/manyoyo",
3
- "version": "5.1.10",
3
+ "version": "5.2.0",
4
4
  "imageVersion": "1.8.1-common",
5
5
  "description": "AI Agent CLI Security Sandbox for Docker and Podman",
6
6
  "keywords": [