@xcanwin/manyoyo 5.5.2 → 5.6.1

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');
@@ -84,7 +86,8 @@ try {
84
86
  XTERM_ADDON_FIT_JS_FILE = null;
85
87
  }
86
88
  try {
87
- MARKED_MIN_JS_FILE = require.resolve('marked/marked.min.js');
89
+ const markedPackageDir = path.dirname(require.resolve('marked/package.json'));
90
+ MARKED_MIN_JS_FILE = path.join(markedPackageDir, 'lib', 'marked.umd.js');
88
91
  } catch (e) {
89
92
  MARKED_MIN_JS_FILE = null;
90
93
  }
@@ -124,7 +127,8 @@ function loadWebSessionHistory(webHistoryDir, containerName) {
124
127
  resumeSupported: false,
125
128
  lastResumeAt: null,
126
129
  lastResumeOk: null,
127
- lastResumeError: ''
130
+ lastResumeError: '',
131
+ applied: null
128
132
  };
129
133
  }
130
134
 
@@ -141,7 +145,10 @@ function loadWebSessionHistory(webHistoryDir, containerName) {
141
145
  resumeSupported: data.resumeSupported === true,
142
146
  lastResumeAt: typeof data.lastResumeAt === 'string' ? data.lastResumeAt : null,
143
147
  lastResumeOk: typeof data.lastResumeOk === 'boolean' ? data.lastResumeOk : null,
144
- lastResumeError: typeof data.lastResumeError === 'string' ? data.lastResumeError : ''
148
+ lastResumeError: typeof data.lastResumeError === 'string' ? data.lastResumeError : '',
149
+ applied: data.applied && typeof data.applied === 'object' && !Array.isArray(data.applied)
150
+ ? data.applied
151
+ : null
145
152
  };
146
153
  } catch (e) {
147
154
  return {
@@ -153,7 +160,8 @@ function loadWebSessionHistory(webHistoryDir, containerName) {
153
160
  resumeSupported: false,
154
161
  lastResumeAt: null,
155
162
  lastResumeOk: null,
156
- lastResumeError: ''
163
+ lastResumeError: '',
164
+ applied: null
157
165
  };
158
166
  }
159
167
  }
@@ -251,6 +259,9 @@ function normalizeAgentPromptCommandTemplate(value, sourceLabel = 'agentPromptCo
251
259
  if (!text.includes('{prompt}')) {
252
260
  throw new Error(`${sourceLabel} 必须包含 {prompt} 占位符`);
253
261
  }
262
+ if (/^codex\s+exec(?:\s|$)/.test(text) && !text.includes('--skip-git-repo-check')) {
263
+ return text.replace(/^codex\s+exec\b/, 'codex exec --skip-git-repo-check');
264
+ }
254
265
  return text;
255
266
  }
256
267
 
@@ -269,6 +280,49 @@ function renderAgentPromptCommand(template, prompt) {
269
280
  return templateText.replace(/\{prompt\}/g, safePrompt);
270
281
  }
271
282
 
283
+ function buildCodexAgentExecCommand(template, prompt) {
284
+ const templateText = normalizeAgentPromptCommandTemplate(template, 'agentPromptCommand');
285
+ const outputFile = `/tmp/manyoyo-web-agent-last-${Date.now()}-${crypto.randomBytes(6).toString('hex')}.txt`;
286
+ const quotedOutputFile = quoteBashSingleValue(outputFile);
287
+ const codexTemplate = templateText.replace(
288
+ /^((?:(?:[A-Za-z_][A-Za-z0-9_]*=)(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s]+)\s+)*)codex\s+exec\b/,
289
+ `$1codex exec --output-last-message ${quotedOutputFile}`
290
+ );
291
+ const command = codexTemplate === templateText
292
+ ? renderAgentPromptCommand(templateText, prompt)
293
+ : renderAgentPromptCommand(codexTemplate, prompt);
294
+ return [
295
+ `rm -f ${quotedOutputFile}`,
296
+ command,
297
+ '__manyoyo_agent_exit=$?',
298
+ `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`,
299
+ `rm -f ${quotedOutputFile}`,
300
+ 'exit $__manyoyo_agent_exit'
301
+ ].join('; ');
302
+ }
303
+
304
+ function buildWebAgentExecCommand(template, prompt, agentProgram) {
305
+ if (agentProgram === 'codex') {
306
+ return buildCodexAgentExecCommand(template, prompt);
307
+ }
308
+ return renderAgentPromptCommand(template, prompt);
309
+ }
310
+
311
+ function extractLastMessageOutput(text) {
312
+ const raw = String(text || '');
313
+ const pattern = new RegExp(
314
+ `(?:^|\\r?\\n)${WEB_AGENT_LAST_MESSAGE_BEGIN_MARKER}\\r?\\n([\\s\\S]*?)(?:\\r?\\n)${WEB_AGENT_LAST_MESSAGE_END_MARKER}(?:\\r?\\n|$)`,
315
+ 'g'
316
+ );
317
+ let lastMatch = null;
318
+ let matched = pattern.exec(raw);
319
+ while (matched) {
320
+ lastMatch = matched[1];
321
+ matched = pattern.exec(raw);
322
+ }
323
+ return String(lastMatch || '').trim();
324
+ }
325
+
272
326
  function getAgentRuntimeMeta(history) {
273
327
  const sessionHistory = history && typeof history === 'object' ? history : {};
274
328
  const template = normalizeAgentPromptCommandTemplate(sessionHistory.agentPromptCommand, 'agentPromptCommand');
@@ -533,15 +587,51 @@ function normalizeStringArray(value, sourceLabel) {
533
587
  .filter(Boolean);
534
588
  }
535
589
 
590
+ function expandHomeAliasPath(filePath) {
591
+ const text = String(filePath || '').trim();
592
+ if (!text) {
593
+ return text;
594
+ }
595
+ const homeDir = os.homedir();
596
+ if (text === '~') {
597
+ return homeDir;
598
+ }
599
+ if (text.startsWith('~/')) {
600
+ return path.join(homeDir, text.slice(2));
601
+ }
602
+ if (text === '$HOME') {
603
+ return homeDir;
604
+ }
605
+ if (text.startsWith('$HOME/')) {
606
+ return path.join(homeDir, text.slice('$HOME/'.length));
607
+ }
608
+ return text;
609
+ }
610
+
611
+ function normalizeVolume(volume) {
612
+ const text = String(volume || '').trim();
613
+ if (!text.startsWith('~') && !text.startsWith('$HOME')) {
614
+ return text;
615
+ }
616
+ const separatorIndex = text.indexOf(':');
617
+ if (separatorIndex === -1) {
618
+ return expandHomeAliasPath(text);
619
+ }
620
+ const hostPath = text.slice(0, separatorIndex);
621
+ const rest = text.slice(separatorIndex);
622
+ return `${expandHomeAliasPath(hostPath)}${rest}`;
623
+ }
624
+
536
625
  function parseEnvFileToArgs(filePath) {
537
- if (!path.isAbsolute(filePath)) {
626
+ const resolvedPath = expandHomeAliasPath(filePath);
627
+ if (!path.isAbsolute(resolvedPath)) {
538
628
  throw new Error(`envFile 仅支持绝对路径: ${filePath}`);
539
629
  }
540
- if (!fs.existsSync(filePath)) {
541
- throw new Error(`未找到环境文件: ${filePath}`);
630
+ if (!fs.existsSync(resolvedPath)) {
631
+ throw new Error(`未找到环境文件: ${resolvedPath}`);
542
632
  }
543
633
 
544
- const content = fs.readFileSync(filePath, 'utf-8');
634
+ const content = fs.readFileSync(resolvedPath, 'utf-8');
545
635
  const args = [];
546
636
  const lines = content.split('\n');
547
637
 
@@ -897,7 +987,7 @@ function buildCreateRuntime(ctx, state, payload) {
897
987
  : normalizeStringArray(config.volumes, 'config.volumes');
898
988
  containerVolumes = [];
899
989
  volumeList.forEach(volume => {
900
- containerVolumes.push('--volume', volume);
990
+ containerVolumes.push('--volume', normalizeVolume(volume));
901
991
  });
902
992
  }
903
993
 
@@ -935,6 +1025,7 @@ function buildCreateRuntime(ctx, state, payload) {
935
1025
  shellPrefix: shellPrefix || '',
936
1026
  shell: shell || '',
937
1027
  shellSuffix: shellSuffix || '',
1028
+ defaultCommand: buildDefaultCommand(shellPrefix, shell, shellSuffix) || '/bin/bash',
938
1029
  agentEnabled: isAgentPromptCommandEnabled(agentPromptCommand),
939
1030
  agentProgram: agentProgram || '',
940
1031
  resumeSupported,
@@ -1056,34 +1147,50 @@ async function execCommandInWebContainer(ctx, containerName, command) {
1056
1147
  );
1057
1148
 
1058
1149
  const MAX_RAW_OUTPUT_CHARS = 32 * 1024 * 1024;
1059
- let rawOutput = '';
1060
- let outputTruncated = false;
1150
+ let stdoutOutput = '';
1151
+ let stderrOutput = '';
1152
+ let stdoutTruncated = false;
1153
+ let stderrTruncated = false;
1061
1154
 
1062
- function appendChunk(chunk) {
1155
+ function appendChunk(chunk, target) {
1063
1156
  if (!chunk) return;
1064
1157
  const text = chunk.toString('utf-8');
1065
1158
  if (!text) return;
1066
- if (rawOutput.length >= MAX_RAW_OUTPUT_CHARS) {
1067
- outputTruncated = true;
1159
+ if (target.value.length >= MAX_RAW_OUTPUT_CHARS) {
1160
+ target.truncated = true;
1068
1161
  return;
1069
1162
  }
1070
- const remain = MAX_RAW_OUTPUT_CHARS - rawOutput.length;
1163
+ const remain = MAX_RAW_OUTPUT_CHARS - target.value.length;
1071
1164
  if (text.length > remain) {
1072
- rawOutput += text.slice(0, remain);
1073
- outputTruncated = true;
1165
+ target.value += text.slice(0, remain);
1166
+ target.truncated = true;
1074
1167
  return;
1075
1168
  }
1076
- rawOutput += text;
1169
+ target.value += text;
1077
1170
  }
1078
1171
 
1079
- process.stdout.on('data', appendChunk);
1080
- process.stderr.on('data', appendChunk);
1172
+ process.stdout.on('data', chunk => appendChunk(chunk, {
1173
+ get value() { return stdoutOutput; },
1174
+ set value(nextValue) { stdoutOutput = nextValue; },
1175
+ get truncated() { return stdoutTruncated; },
1176
+ set truncated(nextValue) { stdoutTruncated = nextValue; }
1177
+ }));
1178
+ process.stderr.on('data', chunk => appendChunk(chunk, {
1179
+ get value() { return stderrOutput; },
1180
+ set value(nextValue) { stderrOutput = nextValue; },
1181
+ get truncated() { return stderrTruncated; },
1182
+ set truncated(nextValue) { stderrTruncated = nextValue; }
1183
+ }));
1081
1184
 
1082
1185
  process.on('error', reject);
1083
1186
  process.on('close', code => {
1084
1187
  const exitCode = typeof code === 'number' ? code : 1;
1085
- const clippedRaw = outputTruncated ? `${rawOutput}\n...[raw-truncated]` : rawOutput;
1086
- const output = clipText(stripAnsi(clippedRaw).trim() || '(无输出)');
1188
+ const clippedStdout = stdoutTruncated ? `${stdoutOutput}\n...[stdout-truncated]` : stdoutOutput;
1189
+ const clippedStderr = stderrTruncated ? `${stderrOutput}\n...[stderr-truncated]` : stderrOutput;
1190
+ const clippedRaw = `${clippedStdout}${clippedStdout && clippedStderr ? '\n' : ''}${clippedStderr}`;
1191
+ const extractedLastMessage = extractLastMessageOutput(clippedStdout);
1192
+ const cleanOutputSource = extractedLastMessage || clippedRaw;
1193
+ const output = clipText(stripAnsi(cleanOutputSource).trim() || '(无输出)');
1087
1194
  resolve({ exitCode, output });
1088
1195
  });
1089
1196
  });
@@ -1169,6 +1276,64 @@ function buildSessionSummary(ctx, state, containerMap, name) {
1169
1276
  };
1170
1277
  }
1171
1278
 
1279
+ function buildSessionFallbackApplied(ctx, state, name, history, summary) {
1280
+ const snapshot = readWebConfigSnapshot(state.webConfigPath);
1281
+ const defaults = buildConfigDefaults(ctx, snapshot.parseError ? {} : snapshot.parsed);
1282
+ const effectiveAgentPromptCommand = history.agentPromptCommand || defaults.agentPromptCommand || '';
1283
+ const effectiveAgentProgram = history.agentProgram || resolveAgentProgram(effectiveAgentPromptCommand) || '';
1284
+ const effectiveResumeSupported = history.resumeSupported === true
1285
+ || Boolean(buildAgentResumeCommand(effectiveAgentProgram));
1286
+ const defaultCommand = buildDefaultCommand(
1287
+ defaults.shellPrefix,
1288
+ defaults.shell,
1289
+ defaults.shellSuffix
1290
+ ) || buildStaticContainerRuntime(ctx, name).defaultCommand;
1291
+
1292
+ return {
1293
+ containerName: name,
1294
+ hostPath: defaults.hostPath || ctx.hostPath || '',
1295
+ containerPath: defaults.containerPath || ctx.containerPath || '',
1296
+ imageName: defaults.imageName || ctx.imageName || '',
1297
+ imageVersion: defaults.imageVersion || ctx.imageVersion || '',
1298
+ containerMode: defaults.containerMode || '',
1299
+ shellPrefix: defaults.shellPrefix || '',
1300
+ shell: defaults.shell || '',
1301
+ shellSuffix: defaults.shellSuffix || '',
1302
+ defaultCommand,
1303
+ agentEnabled: isAgentPromptCommandEnabled(effectiveAgentPromptCommand),
1304
+ agentProgram: effectiveAgentProgram,
1305
+ resumeSupported: effectiveResumeSupported,
1306
+ yolo: defaults.yolo || '',
1307
+ envCount: Object.keys(defaults.env || {}).length,
1308
+ volumeCount: Array.isArray(defaults.volumes) ? defaults.volumes.length : 0,
1309
+ portCount: Array.isArray(defaults.ports) ? defaults.ports.length : 0,
1310
+ status: summary.status || 'history'
1311
+ };
1312
+ }
1313
+
1314
+ function buildSessionDetail(ctx, state, containerMap, name) {
1315
+ const history = loadWebSessionHistory(state.webHistoryDir, name);
1316
+ const normalizedTemplate = normalizeAgentPromptCommandTemplate(history.agentPromptCommand, 'agentPromptCommand');
1317
+ const summary = buildSessionSummary(ctx, state, containerMap, name);
1318
+ const latestMessage = history.messages.length ? history.messages[history.messages.length - 1] : null;
1319
+ const applied = history.applied && typeof history.applied === 'object' && !Array.isArray(history.applied)
1320
+ ? history.applied
1321
+ : buildSessionFallbackApplied(ctx, state, name, history, summary);
1322
+
1323
+ return {
1324
+ ...summary,
1325
+ latestRole: latestMessage && latestMessage.role ? String(latestMessage.role) : '',
1326
+ latestTimestamp: latestMessage && latestMessage.timestamp ? latestMessage.timestamp : summary.updatedAt,
1327
+ agentPromptCommand: normalizedTemplate || '',
1328
+ agentProgram: history.agentProgram || summary.agentProgram || '',
1329
+ resumeSupported: history.resumeSupported === true || summary.resumeSupported === true,
1330
+ lastResumeAt: history.lastResumeAt || null,
1331
+ lastResumeOk: typeof history.lastResumeOk === 'boolean' ? history.lastResumeOk : null,
1332
+ lastResumeError: history.lastResumeError || '',
1333
+ applied
1334
+ };
1335
+ }
1336
+
1172
1337
  function isSafeStaticAssetName(name) {
1173
1338
  return /^[A-Za-z0-9._-]+$/.test(name);
1174
1339
  }
@@ -1584,6 +1749,9 @@ async function handleWebApi(req, res, pathname, ctx, state) {
1584
1749
 
1585
1750
  await ensureWebContainer(ctx, state, runtime);
1586
1751
  setWebSessionAgentPromptCommand(state.webHistoryDir, runtime.containerName, runtime.agentPromptCommand);
1752
+ patchWebSessionAgentState(state.webHistoryDir, runtime.containerName, {
1753
+ applied: runtime.applied
1754
+ });
1587
1755
  sendJson(res, 200, { name: runtime.containerName, applied: runtime.applied });
1588
1756
  }
1589
1757
  },
@@ -1599,6 +1767,20 @@ async function handleWebApi(req, res, pathname, ctx, state) {
1599
1767
  sendJson(res, 200, { name: containerName, messages: history.messages });
1600
1768
  }
1601
1769
  },
1770
+ {
1771
+ method: 'GET',
1772
+ match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/detail$/),
1773
+ handler: async match => {
1774
+ const containerName = getValidSessionName(ctx, res, match[1]);
1775
+ if (!containerName) {
1776
+ return;
1777
+ }
1778
+
1779
+ const containerMap = listWebManyoyoContainers(ctx);
1780
+ const detail = buildSessionDetail(ctx, state, containerMap, containerName);
1781
+ sendJson(res, 200, { name: containerName, detail });
1782
+ }
1783
+ },
1602
1784
  {
1603
1785
  method: 'POST',
1604
1786
  match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/run$/),
@@ -1645,6 +1827,11 @@ async function handleWebApi(req, res, pathname, ctx, state) {
1645
1827
  }
1646
1828
 
1647
1829
  const history = loadWebSessionHistory(state.webHistoryDir, containerName);
1830
+ const normalizedTemplate = normalizeAgentPromptCommandTemplate(history.agentPromptCommand, 'agentPromptCommand');
1831
+ if (normalizedTemplate !== history.agentPromptCommand) {
1832
+ history.agentPromptCommand = normalizedTemplate;
1833
+ saveWebSessionHistory(state.webHistoryDir, containerName, history);
1834
+ }
1648
1835
  if (!isAgentPromptCommandEnabled(history.agentPromptCommand)) {
1649
1836
  sendJson(res, 400, { error: '当前会话未配置 agentPromptCommand' });
1650
1837
  return;
@@ -1669,7 +1856,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
1669
1856
  const effectivePrompt = resumeSucceeded
1670
1857
  ? prompt
1671
1858
  : buildAgentPromptWithHistory(history, prompt);
1672
- const command = renderAgentPromptCommand(history.agentPromptCommand, effectivePrompt);
1859
+ const command = buildWebAgentExecCommand(history.agentPromptCommand, effectivePrompt, agentMeta.agentProgram);
1673
1860
  const contextMode = resumeSucceeded ? 'resume' : (hasPriorConversation ? 'history-injected' : 'first-turn');
1674
1861
  appendWebSessionMessage(state.webHistoryDir, containerName, 'user', prompt, {
1675
1862
  mode: 'agent',
@@ -1995,7 +2182,7 @@ async function startWebServer(options) {
1995
2182
  const { GREEN, CYAN, YELLOW, NC } = ctx.colors;
1996
2183
  const listenHost = formatUrlHost(ctx.serverHost);
1997
2184
  console.log(`${GREEN}✅ MANYOYO Web 服务已启动: http://${listenHost}:${listenPort}${NC}`);
1998
- console.log(`${CYAN}提示: 左侧是 manyoyo 容器会话列表,右侧支持命令模式、AGENT 模式与交互式终端模式。${NC}`);
2185
+ console.log(`${CYAN}提示: 左侧是 manyoyo 容器会话列表,中间是活动/终端/配置/检查工作台,右侧显示当前会话上下文。${NC}`);
1999
2186
  if (ctx.serverHost === '0.0.0.0') {
2000
2187
  console.log(`${CYAN}提示: 当前监听全部网卡,请用本机局域网 IP 访问。${NC}`);
2001
2188
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcanwin/manyoyo",
3
- "version": "5.5.2",
3
+ "version": "5.6.1",
4
4
  "imageVersion": "1.9.0-common",
5
5
  "playwrightCliVersion": "0.1.1",
6
6
  "description": "AI Agent CLI Security Sandbox for Docker and Podman",
@@ -58,25 +58,36 @@
58
58
  "@playwright/mcp": "0.0.68",
59
59
  "@xterm/addon-fit": "^0.11.0",
60
60
  "@xterm/xterm": "^6.0.0",
61
- "commander": "^12.0.0",
61
+ "commander": "^14.0.3",
62
62
  "json5": "^2.2.3",
63
- "marked": "^12.0.2",
63
+ "marked": "^17.0.5",
64
64
  "playwright": "1.58.2",
65
- "ws": "^8.19.0"
65
+ "ws": "^8.20.0"
66
66
  },
67
67
  "devDependencies": {
68
- "jest": "^30.2.0",
69
- "vitepress": "^2.0.0-alpha.16"
68
+ "jest": "^30.3.0",
69
+ "vitepress": "^1.6.4"
70
70
  },
71
71
  "overrides": {
72
+ "esbuild": "^0.25.12",
72
73
  "glob": "^13.0.6",
73
74
  "minimatch": "^10.2.2",
74
- "test-exclude": "^8.0.0"
75
+ "test-exclude": "^8.0.0",
76
+ "vite": "^6.4.1"
75
77
  },
76
78
  "jest": {
77
79
  "testMatch": [
78
80
  "**/test/**/*.test.js"
79
81
  ],
80
- "testEnvironment": "node"
82
+ "testEnvironment": "node",
83
+ "modulePathIgnorePatterns": [
84
+ "<rootDir>/temp/"
85
+ ],
86
+ "testPathIgnorePatterns": [
87
+ "<rootDir>/temp/"
88
+ ],
89
+ "watchPathIgnorePatterns": [
90
+ "<rootDir>/temp/"
91
+ ]
81
92
  }
82
93
  }