@xcanwin/manyoyo 5.4.14 → 5.6.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/lib/web/server.js CHANGED
@@ -26,6 +26,8 @@ const WEB_TERMINAL_MIN_ROWS = 12;
26
26
  const WEB_AGENT_CONTEXT_MAX_MESSAGES = 24;
27
27
  const WEB_AGENT_CONTEXT_MAX_CHARS = 6000;
28
28
  const WEB_AGENT_CONTEXT_PER_MESSAGE_MAX_CHARS = 600;
29
+ const WEB_AGENT_LAST_MESSAGE_BEGIN_MARKER = '__MANYOYO_LAST_MESSAGE_BEGIN__';
30
+ const WEB_AGENT_LAST_MESSAGE_END_MARKER = '__MANYOYO_LAST_MESSAGE_END__';
29
31
  const WEB_AUTH_COOKIE_NAME = 'manyoyo_web_auth';
30
32
  const WEB_AUTH_TTL_SECONDS = 12 * 60 * 60;
31
33
  const FRONTEND_DIR = path.join(__dirname, 'frontend');
@@ -124,7 +126,8 @@ function loadWebSessionHistory(webHistoryDir, containerName) {
124
126
  resumeSupported: false,
125
127
  lastResumeAt: null,
126
128
  lastResumeOk: null,
127
- lastResumeError: ''
129
+ lastResumeError: '',
130
+ applied: null
128
131
  };
129
132
  }
130
133
 
@@ -141,7 +144,10 @@ function loadWebSessionHistory(webHistoryDir, containerName) {
141
144
  resumeSupported: data.resumeSupported === true,
142
145
  lastResumeAt: typeof data.lastResumeAt === 'string' ? data.lastResumeAt : null,
143
146
  lastResumeOk: typeof data.lastResumeOk === 'boolean' ? data.lastResumeOk : null,
144
- lastResumeError: typeof data.lastResumeError === 'string' ? data.lastResumeError : ''
147
+ lastResumeError: typeof data.lastResumeError === 'string' ? data.lastResumeError : '',
148
+ applied: data.applied && typeof data.applied === 'object' && !Array.isArray(data.applied)
149
+ ? data.applied
150
+ : null
145
151
  };
146
152
  } catch (e) {
147
153
  return {
@@ -153,7 +159,8 @@ function loadWebSessionHistory(webHistoryDir, containerName) {
153
159
  resumeSupported: false,
154
160
  lastResumeAt: null,
155
161
  lastResumeOk: null,
156
- lastResumeError: ''
162
+ lastResumeError: '',
163
+ applied: null
157
164
  };
158
165
  }
159
166
  }
@@ -251,6 +258,9 @@ function normalizeAgentPromptCommandTemplate(value, sourceLabel = 'agentPromptCo
251
258
  if (!text.includes('{prompt}')) {
252
259
  throw new Error(`${sourceLabel} 必须包含 {prompt} 占位符`);
253
260
  }
261
+ if (/^codex\s+exec(?:\s|$)/.test(text) && !text.includes('--skip-git-repo-check')) {
262
+ return text.replace(/^codex\s+exec\b/, 'codex exec --skip-git-repo-check');
263
+ }
254
264
  return text;
255
265
  }
256
266
 
@@ -269,6 +279,49 @@ function renderAgentPromptCommand(template, prompt) {
269
279
  return templateText.replace(/\{prompt\}/g, safePrompt);
270
280
  }
271
281
 
282
+ function buildCodexAgentExecCommand(template, prompt) {
283
+ const templateText = normalizeAgentPromptCommandTemplate(template, 'agentPromptCommand');
284
+ const outputFile = `/tmp/manyoyo-web-agent-last-${Date.now()}-${crypto.randomBytes(6).toString('hex')}.txt`;
285
+ const quotedOutputFile = quoteBashSingleValue(outputFile);
286
+ const codexTemplate = templateText.replace(
287
+ /^((?:(?:[A-Za-z_][A-Za-z0-9_]*=)(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s]+)\s+)*)codex\s+exec\b/,
288
+ `$1codex exec --output-last-message ${quotedOutputFile}`
289
+ );
290
+ const command = codexTemplate === templateText
291
+ ? renderAgentPromptCommand(templateText, prompt)
292
+ : renderAgentPromptCommand(codexTemplate, prompt);
293
+ return [
294
+ `rm -f ${quotedOutputFile}`,
295
+ command,
296
+ '__manyoyo_agent_exit=$?',
297
+ `if [ -f ${quotedOutputFile} ]; then printf '\\n${WEB_AGENT_LAST_MESSAGE_BEGIN_MARKER}\\n'; cat ${quotedOutputFile}; printf '\\n${WEB_AGENT_LAST_MESSAGE_END_MARKER}\\n'; fi`,
298
+ `rm -f ${quotedOutputFile}`,
299
+ 'exit $__manyoyo_agent_exit'
300
+ ].join('; ');
301
+ }
302
+
303
+ function buildWebAgentExecCommand(template, prompt, agentProgram) {
304
+ if (agentProgram === 'codex') {
305
+ return buildCodexAgentExecCommand(template, prompt);
306
+ }
307
+ return renderAgentPromptCommand(template, prompt);
308
+ }
309
+
310
+ function extractLastMessageOutput(text) {
311
+ const raw = String(text || '');
312
+ const pattern = new RegExp(
313
+ `(?:^|\\r?\\n)${WEB_AGENT_LAST_MESSAGE_BEGIN_MARKER}\\r?\\n([\\s\\S]*?)(?:\\r?\\n)${WEB_AGENT_LAST_MESSAGE_END_MARKER}(?:\\r?\\n|$)`,
314
+ 'g'
315
+ );
316
+ let lastMatch = null;
317
+ let matched = pattern.exec(raw);
318
+ while (matched) {
319
+ lastMatch = matched[1];
320
+ matched = pattern.exec(raw);
321
+ }
322
+ return String(lastMatch || '').trim();
323
+ }
324
+
272
325
  function getAgentRuntimeMeta(history) {
273
326
  const sessionHistory = history && typeof history === 'object' ? history : {};
274
327
  const template = normalizeAgentPromptCommandTemplate(sessionHistory.agentPromptCommand, 'agentPromptCommand');
@@ -533,15 +586,51 @@ function normalizeStringArray(value, sourceLabel) {
533
586
  .filter(Boolean);
534
587
  }
535
588
 
589
+ function expandHomeAliasPath(filePath) {
590
+ const text = String(filePath || '').trim();
591
+ if (!text) {
592
+ return text;
593
+ }
594
+ const homeDir = os.homedir();
595
+ if (text === '~') {
596
+ return homeDir;
597
+ }
598
+ if (text.startsWith('~/')) {
599
+ return path.join(homeDir, text.slice(2));
600
+ }
601
+ if (text === '$HOME') {
602
+ return homeDir;
603
+ }
604
+ if (text.startsWith('$HOME/')) {
605
+ return path.join(homeDir, text.slice('$HOME/'.length));
606
+ }
607
+ return text;
608
+ }
609
+
610
+ function normalizeVolume(volume) {
611
+ const text = String(volume || '').trim();
612
+ if (!text.startsWith('~') && !text.startsWith('$HOME')) {
613
+ return text;
614
+ }
615
+ const separatorIndex = text.indexOf(':');
616
+ if (separatorIndex === -1) {
617
+ return expandHomeAliasPath(text);
618
+ }
619
+ const hostPath = text.slice(0, separatorIndex);
620
+ const rest = text.slice(separatorIndex);
621
+ return `${expandHomeAliasPath(hostPath)}${rest}`;
622
+ }
623
+
536
624
  function parseEnvFileToArgs(filePath) {
537
- if (!path.isAbsolute(filePath)) {
625
+ const resolvedPath = expandHomeAliasPath(filePath);
626
+ if (!path.isAbsolute(resolvedPath)) {
538
627
  throw new Error(`envFile 仅支持绝对路径: ${filePath}`);
539
628
  }
540
- if (!fs.existsSync(filePath)) {
541
- throw new Error(`未找到环境文件: ${filePath}`);
629
+ if (!fs.existsSync(resolvedPath)) {
630
+ throw new Error(`未找到环境文件: ${resolvedPath}`);
542
631
  }
543
632
 
544
- const content = fs.readFileSync(filePath, 'utf-8');
633
+ const content = fs.readFileSync(resolvedPath, 'utf-8');
545
634
  const args = [];
546
635
  const lines = content.split('\n');
547
636
 
@@ -897,7 +986,7 @@ function buildCreateRuntime(ctx, state, payload) {
897
986
  : normalizeStringArray(config.volumes, 'config.volumes');
898
987
  containerVolumes = [];
899
988
  volumeList.forEach(volume => {
900
- containerVolumes.push('--volume', volume);
989
+ containerVolumes.push('--volume', normalizeVolume(volume));
901
990
  });
902
991
  }
903
992
 
@@ -935,6 +1024,7 @@ function buildCreateRuntime(ctx, state, payload) {
935
1024
  shellPrefix: shellPrefix || '',
936
1025
  shell: shell || '',
937
1026
  shellSuffix: shellSuffix || '',
1027
+ defaultCommand: buildDefaultCommand(shellPrefix, shell, shellSuffix) || '/bin/bash',
938
1028
  agentEnabled: isAgentPromptCommandEnabled(agentPromptCommand),
939
1029
  agentProgram: agentProgram || '',
940
1030
  resumeSupported,
@@ -1083,7 +1173,9 @@ async function execCommandInWebContainer(ctx, containerName, command) {
1083
1173
  process.on('close', code => {
1084
1174
  const exitCode = typeof code === 'number' ? code : 1;
1085
1175
  const clippedRaw = outputTruncated ? `${rawOutput}\n...[raw-truncated]` : rawOutput;
1086
- const output = clipText(stripAnsi(clippedRaw).trim() || '(无输出)');
1176
+ const extractedLastMessage = extractLastMessageOutput(clippedRaw);
1177
+ const cleanOutputSource = extractedLastMessage || clippedRaw;
1178
+ const output = clipText(stripAnsi(cleanOutputSource).trim() || '(无输出)');
1087
1179
  resolve({ exitCode, output });
1088
1180
  });
1089
1181
  });
@@ -1169,6 +1261,64 @@ function buildSessionSummary(ctx, state, containerMap, name) {
1169
1261
  };
1170
1262
  }
1171
1263
 
1264
+ function buildSessionFallbackApplied(ctx, state, name, history, summary) {
1265
+ const snapshot = readWebConfigSnapshot(state.webConfigPath);
1266
+ const defaults = buildConfigDefaults(ctx, snapshot.parseError ? {} : snapshot.parsed);
1267
+ const effectiveAgentPromptCommand = history.agentPromptCommand || defaults.agentPromptCommand || '';
1268
+ const effectiveAgentProgram = history.agentProgram || resolveAgentProgram(effectiveAgentPromptCommand) || '';
1269
+ const effectiveResumeSupported = history.resumeSupported === true
1270
+ || Boolean(buildAgentResumeCommand(effectiveAgentProgram));
1271
+ const defaultCommand = buildDefaultCommand(
1272
+ defaults.shellPrefix,
1273
+ defaults.shell,
1274
+ defaults.shellSuffix
1275
+ ) || buildStaticContainerRuntime(ctx, name).defaultCommand;
1276
+
1277
+ return {
1278
+ containerName: name,
1279
+ hostPath: defaults.hostPath || ctx.hostPath || '',
1280
+ containerPath: defaults.containerPath || ctx.containerPath || '',
1281
+ imageName: defaults.imageName || ctx.imageName || '',
1282
+ imageVersion: defaults.imageVersion || ctx.imageVersion || '',
1283
+ containerMode: defaults.containerMode || '',
1284
+ shellPrefix: defaults.shellPrefix || '',
1285
+ shell: defaults.shell || '',
1286
+ shellSuffix: defaults.shellSuffix || '',
1287
+ defaultCommand,
1288
+ agentEnabled: isAgentPromptCommandEnabled(effectiveAgentPromptCommand),
1289
+ agentProgram: effectiveAgentProgram,
1290
+ resumeSupported: effectiveResumeSupported,
1291
+ yolo: defaults.yolo || '',
1292
+ envCount: Object.keys(defaults.env || {}).length,
1293
+ volumeCount: Array.isArray(defaults.volumes) ? defaults.volumes.length : 0,
1294
+ portCount: Array.isArray(defaults.ports) ? defaults.ports.length : 0,
1295
+ status: summary.status || 'history'
1296
+ };
1297
+ }
1298
+
1299
+ function buildSessionDetail(ctx, state, containerMap, name) {
1300
+ const history = loadWebSessionHistory(state.webHistoryDir, name);
1301
+ const normalizedTemplate = normalizeAgentPromptCommandTemplate(history.agentPromptCommand, 'agentPromptCommand');
1302
+ const summary = buildSessionSummary(ctx, state, containerMap, name);
1303
+ const latestMessage = history.messages.length ? history.messages[history.messages.length - 1] : null;
1304
+ const applied = history.applied && typeof history.applied === 'object' && !Array.isArray(history.applied)
1305
+ ? history.applied
1306
+ : buildSessionFallbackApplied(ctx, state, name, history, summary);
1307
+
1308
+ return {
1309
+ ...summary,
1310
+ latestRole: latestMessage && latestMessage.role ? String(latestMessage.role) : '',
1311
+ latestTimestamp: latestMessage && latestMessage.timestamp ? latestMessage.timestamp : summary.updatedAt,
1312
+ agentPromptCommand: normalizedTemplate || '',
1313
+ agentProgram: history.agentProgram || summary.agentProgram || '',
1314
+ resumeSupported: history.resumeSupported === true || summary.resumeSupported === true,
1315
+ lastResumeAt: history.lastResumeAt || null,
1316
+ lastResumeOk: typeof history.lastResumeOk === 'boolean' ? history.lastResumeOk : null,
1317
+ lastResumeError: history.lastResumeError || '',
1318
+ applied
1319
+ };
1320
+ }
1321
+
1172
1322
  function isSafeStaticAssetName(name) {
1173
1323
  return /^[A-Za-z0-9._-]+$/.test(name);
1174
1324
  }
@@ -1584,6 +1734,9 @@ async function handleWebApi(req, res, pathname, ctx, state) {
1584
1734
 
1585
1735
  await ensureWebContainer(ctx, state, runtime);
1586
1736
  setWebSessionAgentPromptCommand(state.webHistoryDir, runtime.containerName, runtime.agentPromptCommand);
1737
+ patchWebSessionAgentState(state.webHistoryDir, runtime.containerName, {
1738
+ applied: runtime.applied
1739
+ });
1587
1740
  sendJson(res, 200, { name: runtime.containerName, applied: runtime.applied });
1588
1741
  }
1589
1742
  },
@@ -1599,6 +1752,20 @@ async function handleWebApi(req, res, pathname, ctx, state) {
1599
1752
  sendJson(res, 200, { name: containerName, messages: history.messages });
1600
1753
  }
1601
1754
  },
1755
+ {
1756
+ method: 'GET',
1757
+ match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/detail$/),
1758
+ handler: async match => {
1759
+ const containerName = getValidSessionName(ctx, res, match[1]);
1760
+ if (!containerName) {
1761
+ return;
1762
+ }
1763
+
1764
+ const containerMap = listWebManyoyoContainers(ctx);
1765
+ const detail = buildSessionDetail(ctx, state, containerMap, containerName);
1766
+ sendJson(res, 200, { name: containerName, detail });
1767
+ }
1768
+ },
1602
1769
  {
1603
1770
  method: 'POST',
1604
1771
  match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/run$/),
@@ -1645,6 +1812,11 @@ async function handleWebApi(req, res, pathname, ctx, state) {
1645
1812
  }
1646
1813
 
1647
1814
  const history = loadWebSessionHistory(state.webHistoryDir, containerName);
1815
+ const normalizedTemplate = normalizeAgentPromptCommandTemplate(history.agentPromptCommand, 'agentPromptCommand');
1816
+ if (normalizedTemplate !== history.agentPromptCommand) {
1817
+ history.agentPromptCommand = normalizedTemplate;
1818
+ saveWebSessionHistory(state.webHistoryDir, containerName, history);
1819
+ }
1648
1820
  if (!isAgentPromptCommandEnabled(history.agentPromptCommand)) {
1649
1821
  sendJson(res, 400, { error: '当前会话未配置 agentPromptCommand' });
1650
1822
  return;
@@ -1669,7 +1841,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
1669
1841
  const effectivePrompt = resumeSucceeded
1670
1842
  ? prompt
1671
1843
  : buildAgentPromptWithHistory(history, prompt);
1672
- const command = renderAgentPromptCommand(history.agentPromptCommand, effectivePrompt);
1844
+ const command = buildWebAgentExecCommand(history.agentPromptCommand, effectivePrompt, agentMeta.agentProgram);
1673
1845
  const contextMode = resumeSucceeded ? 'resume' : (hasPriorConversation ? 'history-injected' : 'first-turn');
1674
1846
  appendWebSessionMessage(state.webHistoryDir, containerName, 'user', prompt, {
1675
1847
  mode: 'agent',
@@ -1995,7 +2167,7 @@ async function startWebServer(options) {
1995
2167
  const { GREEN, CYAN, YELLOW, NC } = ctx.colors;
1996
2168
  const listenHost = formatUrlHost(ctx.serverHost);
1997
2169
  console.log(`${GREEN}✅ MANYOYO Web 服务已启动: http://${listenHost}:${listenPort}${NC}`);
1998
- console.log(`${CYAN}提示: 左侧是 manyoyo 容器会话列表,右侧支持命令模式、AGENT 模式与交互式终端模式。${NC}`);
2170
+ console.log(`${CYAN}提示: 左侧是 manyoyo 容器会话列表,中间是活动/终端/配置/检查工作台,右侧显示当前会话上下文。${NC}`);
1999
2171
  if (ctx.serverHost === '0.0.0.0') {
2000
2172
  console.log(`${CYAN}提示: 当前监听全部网卡,请用本机局域网 IP 访问。${NC}`);
2001
2173
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xcanwin/manyoyo",
3
- "version": "5.4.14",
4
- "imageVersion": "1.8.12-common",
3
+ "version": "5.6.0",
4
+ "imageVersion": "1.9.0-common",
5
5
  "playwrightCliVersion": "0.1.1",
6
6
  "description": "AI Agent CLI Security Sandbox for Docker and Podman",
7
7
  "keywords": [
@@ -77,6 +77,15 @@
77
77
  "testMatch": [
78
78
  "**/test/**/*.test.js"
79
79
  ],
80
- "testEnvironment": "node"
80
+ "testEnvironment": "node",
81
+ "modulePathIgnorePatterns": [
82
+ "<rootDir>/temp/"
83
+ ],
84
+ "testPathIgnorePatterns": [
85
+ "<rootDir>/temp/"
86
+ ],
87
+ "watchPathIgnorePatterns": [
88
+ "<rootDir>/temp/"
89
+ ]
81
90
  }
82
91
  }