@xcanwin/manyoyo 5.5.2 → 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/agent-resume.js +1 -1
- package/lib/web/frontend/app.css +250 -30
- package/lib/web/frontend/app.html +62 -47
- package/lib/web/frontend/app.js +353 -79
- package/lib/web/server.js +183 -11
- package/package.json +11 -2
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
|
-
|
|
625
|
+
const resolvedPath = expandHomeAliasPath(filePath);
|
|
626
|
+
if (!path.isAbsolute(resolvedPath)) {
|
|
538
627
|
throw new Error(`envFile 仅支持绝对路径: ${filePath}`);
|
|
539
628
|
}
|
|
540
|
-
if (!fs.existsSync(
|
|
541
|
-
throw new Error(`未找到环境文件: ${
|
|
629
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
630
|
+
throw new Error(`未找到环境文件: ${resolvedPath}`);
|
|
542
631
|
}
|
|
543
632
|
|
|
544
|
-
const content = fs.readFileSync(
|
|
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
|
|
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 =
|
|
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
|
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xcanwin/manyoyo",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.6.0",
|
|
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",
|
|
@@ -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
|
}
|