@xcanwin/manyoyo 5.2.0 → 5.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/web/frontend/app.css +33 -12
- package/lib/web/frontend/app.html +5 -2
- package/lib/web/frontend/app.js +57 -8
- package/lib/web/frontend/markdown-renderer.js +115 -0
- package/lib/web/frontend/markdown.css +76 -0
- package/lib/web/server.js +177 -11
- package/package.json +2 -1
package/lib/web/frontend/app.css
CHANGED
|
@@ -437,20 +437,24 @@ textarea:focus-visible {
|
|
|
437
437
|
box-shadow: inset 3px 0 0 var(--accent), 0 0 0 2px rgba(196, 85, 31, 0.14);
|
|
438
438
|
}
|
|
439
439
|
|
|
440
|
-
.session-item.history
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
.session-item.history-only .session-time {
|
|
447
|
-
color: #b3ab9f;
|
|
440
|
+
.session-item.status-history:not(.active),
|
|
441
|
+
.session-item.status-stopped:not(.active),
|
|
442
|
+
.session-item.status-unknown:not(.active) {
|
|
443
|
+
border-color: rgba(181, 146, 99, 0.22);
|
|
444
|
+
background: rgba(255, 252, 247, 0.72);
|
|
445
|
+
box-shadow: none;
|
|
448
446
|
}
|
|
449
447
|
|
|
450
|
-
.session-item.history
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
448
|
+
.session-item.status-history:not(.active) .session-name,
|
|
449
|
+
.session-item.status-history:not(.active) .session-meta,
|
|
450
|
+
.session-item.status-history:not(.active) .session-time,
|
|
451
|
+
.session-item.status-stopped:not(.active) .session-name,
|
|
452
|
+
.session-item.status-stopped:not(.active) .session-meta,
|
|
453
|
+
.session-item.status-stopped:not(.active) .session-time,
|
|
454
|
+
.session-item.status-unknown:not(.active) .session-name,
|
|
455
|
+
.session-item.status-unknown:not(.active) .session-meta,
|
|
456
|
+
.session-item.status-unknown:not(.active) .session-time {
|
|
457
|
+
opacity: 0.25;
|
|
454
458
|
}
|
|
455
459
|
|
|
456
460
|
.session-name {
|
|
@@ -847,6 +851,23 @@ body.terminal-mode .composer {
|
|
|
847
851
|
opacity: 0.78;
|
|
848
852
|
}
|
|
849
853
|
|
|
854
|
+
body.agent-mode .msg.origin-command .bubble,
|
|
855
|
+
body.agent-mode .msg.origin-command .msg-meta,
|
|
856
|
+
body.agent-mode .msg.origin-command .msg-exit {
|
|
857
|
+
opacity: 0.25;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
body.command-mode .msg.origin-agent .bubble,
|
|
861
|
+
body.command-mode .msg.origin-agent .msg-meta,
|
|
862
|
+
body.command-mode .msg.origin-agent .msg-exit {
|
|
863
|
+
opacity: 0.25;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
body.agent-mode .msg.origin-command .bubble,
|
|
867
|
+
body.command-mode .msg.origin-agent .bubble {
|
|
868
|
+
box-shadow: none;
|
|
869
|
+
}
|
|
870
|
+
|
|
850
871
|
.bubble pre {
|
|
851
872
|
margin: 0;
|
|
852
873
|
white-space: pre-wrap;
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
/>
|
|
9
9
|
<title>MANYOYO Web</title>
|
|
10
10
|
<link rel="stylesheet" href="/app/frontend/app.css" />
|
|
11
|
+
<link rel="stylesheet" href="/app/frontend/markdown.css" />
|
|
11
12
|
<link rel="stylesheet" href="/app/vendor/xterm.css" />
|
|
12
13
|
</head>
|
|
13
14
|
<body>
|
|
@@ -65,8 +66,8 @@
|
|
|
65
66
|
</header>
|
|
66
67
|
<section class="mode-switch" id="modeSwitch">
|
|
67
68
|
<div class="mode-switch-left">
|
|
68
|
-
<button type="button" id="
|
|
69
|
-
<button type="button" id="
|
|
69
|
+
<button type="button" id="modeAgentBtn" class="secondary is-active">AGENT 模式</button>
|
|
70
|
+
<button type="button" id="modeCommandBtn" class="secondary">命令模式</button>
|
|
70
71
|
<button type="button" id="modeTerminalBtn" class="secondary">交互终端</button>
|
|
71
72
|
</div>
|
|
72
73
|
<div class="mode-terminal-controls">
|
|
@@ -164,6 +165,8 @@
|
|
|
164
165
|
|
|
165
166
|
<script src="/app/vendor/xterm.js"></script>
|
|
166
167
|
<script src="/app/vendor/xterm-addon-fit.js"></script>
|
|
168
|
+
<script src="/app/vendor/marked.min.js"></script>
|
|
169
|
+
<script src="/app/frontend/markdown-renderer.js"></script>
|
|
167
170
|
<script src="/app/frontend/app.js"></script>
|
|
168
171
|
</body>
|
|
169
172
|
</html>
|
package/lib/web/frontend/app.js
CHANGED
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
active: '',
|
|
41
41
|
messages: [],
|
|
42
42
|
messageRenderKeys: [],
|
|
43
|
-
mode: '
|
|
43
|
+
mode: 'agent',
|
|
44
44
|
sending: false,
|
|
45
45
|
loadingSessions: false,
|
|
46
46
|
loadingMessages: false,
|
|
@@ -155,9 +155,20 @@
|
|
|
155
155
|
codex: 'codex exec {prompt}',
|
|
156
156
|
opencode: 'opencode run {prompt}'
|
|
157
157
|
};
|
|
158
|
+
const markdownRenderer = window.ManyoyoMarkdown
|
|
159
|
+
&& typeof window.ManyoyoMarkdown.shouldRenderMessage === 'function'
|
|
160
|
+
&& typeof window.ManyoyoMarkdown.render === 'function'
|
|
161
|
+
? window.ManyoyoMarkdown
|
|
162
|
+
: null;
|
|
163
|
+
|
|
164
|
+
function appendPlainMessageContent(bubble, content) {
|
|
165
|
+
const pre = document.createElement('pre');
|
|
166
|
+
pre.textContent = content == null ? '' : String(content);
|
|
167
|
+
bubble.appendChild(pre);
|
|
168
|
+
}
|
|
158
169
|
|
|
159
170
|
function roleName(role, message) {
|
|
160
|
-
if (role === 'user') return '
|
|
171
|
+
if (role === 'user') return '我';
|
|
161
172
|
if (role === 'assistant') {
|
|
162
173
|
if (message && message.mode === 'agent') {
|
|
163
174
|
return 'AGENT 回复';
|
|
@@ -934,7 +945,7 @@
|
|
|
934
945
|
removeBtn.disabled = !state.active || busy;
|
|
935
946
|
removeAllBtn.disabled = !state.active || busy;
|
|
936
947
|
sendBtn.disabled = !composerMode || !state.active || busy || (agentMode && !agentEnabled);
|
|
937
|
-
commandInput.disabled = !composerMode || !state.active ||
|
|
948
|
+
commandInput.disabled = !composerMode || !state.active || (agentMode && !agentEnabled);
|
|
938
949
|
if (commandInput) {
|
|
939
950
|
commandInput.placeholder = agentMode
|
|
940
951
|
? '输入提示词,例如:请帮我分析当前项目结构并给出重构建议'
|
|
@@ -1146,6 +1157,10 @@
|
|
|
1146
1157
|
row.style.setProperty('--item-index', String(index));
|
|
1147
1158
|
row.classList.toggle('active', state.active === session.name);
|
|
1148
1159
|
row.classList.toggle('history-only', status.tone === 'history');
|
|
1160
|
+
row.classList.toggle('status-running', status.tone === 'running');
|
|
1161
|
+
row.classList.toggle('status-stopped', status.tone === 'stopped');
|
|
1162
|
+
row.classList.toggle('status-history', status.tone === 'history');
|
|
1163
|
+
row.classList.toggle('status-unknown', status.tone === 'unknown');
|
|
1149
1164
|
if (row.__sessionNameNode) {
|
|
1150
1165
|
row.__sessionNameNode.textContent = session.name;
|
|
1151
1166
|
}
|
|
@@ -1329,16 +1344,33 @@
|
|
|
1329
1344
|
return `id:${msg.id}`;
|
|
1330
1345
|
}
|
|
1331
1346
|
const role = msg && msg.role ? String(msg.role) : '';
|
|
1347
|
+
const mode = msg && msg.mode ? String(msg.mode) : '';
|
|
1332
1348
|
const timestamp = msg && msg.timestamp ? String(msg.timestamp) : '';
|
|
1333
1349
|
const exitCode = msg && typeof msg.exitCode === 'number' ? String(msg.exitCode) : '';
|
|
1334
1350
|
const pending = msg && msg.pending ? '1' : '0';
|
|
1335
1351
|
const content = msg && msg.content ? String(msg.content) : '';
|
|
1336
|
-
return `idx:${index}|${role}|${timestamp}|${exitCode}|${pending}|${content}`;
|
|
1352
|
+
return `idx:${index}|${role}|${mode}|${timestamp}|${exitCode}|${pending}|${content}`;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
function resolveMessageOrigin(msg) {
|
|
1356
|
+
const role = msg && msg.role ? String(msg.role) : '';
|
|
1357
|
+
if (!(role === 'user' || role === 'assistant')) {
|
|
1358
|
+
return '';
|
|
1359
|
+
}
|
|
1360
|
+
const mode = msg && msg.mode ? String(msg.mode) : '';
|
|
1361
|
+
if (mode === 'agent') {
|
|
1362
|
+
return 'agent';
|
|
1363
|
+
}
|
|
1364
|
+
return 'command';
|
|
1337
1365
|
}
|
|
1338
1366
|
|
|
1339
1367
|
function createMessageRow(msg, index) {
|
|
1340
1368
|
const row = document.createElement('article');
|
|
1341
1369
|
row.className = 'msg ' + (msg.role || 'system') + (msg.pending ? ' pending' : '');
|
|
1370
|
+
const origin = resolveMessageOrigin(msg);
|
|
1371
|
+
if (origin) {
|
|
1372
|
+
row.classList.add('origin-' + origin);
|
|
1373
|
+
}
|
|
1342
1374
|
row.style.setProperty('--msg-index', String(index));
|
|
1343
1375
|
|
|
1344
1376
|
const meta = document.createElement('div');
|
|
@@ -1354,9 +1386,25 @@
|
|
|
1354
1386
|
const bubble = document.createElement('div');
|
|
1355
1387
|
bubble.className = 'bubble';
|
|
1356
1388
|
|
|
1357
|
-
const
|
|
1358
|
-
|
|
1359
|
-
|
|
1389
|
+
const shouldRenderMarkdown = Boolean(markdownRenderer && markdownRenderer.shouldRenderMessage(msg));
|
|
1390
|
+
if (shouldRenderMarkdown) {
|
|
1391
|
+
const markdownNode = document.createElement('div');
|
|
1392
|
+
markdownNode.className = 'md-content';
|
|
1393
|
+
let renderedMarkdown = '';
|
|
1394
|
+
try {
|
|
1395
|
+
renderedMarkdown = String(markdownRenderer.render(msg.content) || '');
|
|
1396
|
+
} catch (e) {
|
|
1397
|
+
renderedMarkdown = '';
|
|
1398
|
+
}
|
|
1399
|
+
if (renderedMarkdown) {
|
|
1400
|
+
markdownNode.innerHTML = renderedMarkdown;
|
|
1401
|
+
bubble.appendChild(markdownNode);
|
|
1402
|
+
} else {
|
|
1403
|
+
appendPlainMessageContent(bubble, msg.content);
|
|
1404
|
+
}
|
|
1405
|
+
} else {
|
|
1406
|
+
appendPlainMessageContent(bubble, msg.content);
|
|
1407
|
+
}
|
|
1360
1408
|
|
|
1361
1409
|
row.appendChild(meta);
|
|
1362
1410
|
row.appendChild(bubble);
|
|
@@ -1703,6 +1751,7 @@
|
|
|
1703
1751
|
})
|
|
1704
1752
|
});
|
|
1705
1753
|
closeCreateModal();
|
|
1754
|
+
state.mode = 'agent';
|
|
1706
1755
|
await loadSessions(data.name);
|
|
1707
1756
|
if (isMobileLayout()) {
|
|
1708
1757
|
closeMobileSessionPanel();
|
|
@@ -1993,7 +2042,7 @@
|
|
|
1993
2042
|
renderSessions();
|
|
1994
2043
|
renderMessages(state.messages);
|
|
1995
2044
|
setMobileSessionPanel(false);
|
|
1996
|
-
document.body.classList.add('
|
|
2045
|
+
document.body.classList.add('agent-mode');
|
|
1997
2046
|
syncUi();
|
|
1998
2047
|
loadSessions().catch(function (e) {
|
|
1999
2048
|
alert(e.message);
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
const MARKDOWN_URL_PROTOCOL_PATTERN = /^(https?:|mailto:|tel:)/i;
|
|
3
|
+
const runtime = {
|
|
4
|
+
configured: false,
|
|
5
|
+
available: false
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
function escapeHtml(value) {
|
|
9
|
+
return String(value == null ? '' : value)
|
|
10
|
+
.replace(/&/g, '&')
|
|
11
|
+
.replace(/</g, '<')
|
|
12
|
+
.replace(/>/g, '>')
|
|
13
|
+
.replace(/"/g, '"')
|
|
14
|
+
.replace(/'/g, ''');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function sanitizeMarkdownUrl(value) {
|
|
18
|
+
const raw = String(value == null ? '' : value).trim();
|
|
19
|
+
if (!raw) {
|
|
20
|
+
return '';
|
|
21
|
+
}
|
|
22
|
+
if (raw[0] === '#') {
|
|
23
|
+
return raw;
|
|
24
|
+
}
|
|
25
|
+
if (raw[0] === '/') {
|
|
26
|
+
return raw.startsWith('//') ? '' : raw;
|
|
27
|
+
}
|
|
28
|
+
if (raw.startsWith('./') || raw.startsWith('../')) {
|
|
29
|
+
return raw;
|
|
30
|
+
}
|
|
31
|
+
if (MARKDOWN_URL_PROTOCOL_PATTERN.test(raw)) {
|
|
32
|
+
return raw;
|
|
33
|
+
}
|
|
34
|
+
return '';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getMarkedApi() {
|
|
38
|
+
const api = window.marked;
|
|
39
|
+
if (!api || typeof api.parse !== 'function') {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return api;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function ensureRendererConfigured() {
|
|
46
|
+
if (runtime.configured) {
|
|
47
|
+
return runtime.available;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const markedApi = getMarkedApi();
|
|
51
|
+
if (!markedApi || typeof markedApi.Renderer !== 'function' || typeof markedApi.use !== 'function') {
|
|
52
|
+
runtime.configured = true;
|
|
53
|
+
runtime.available = false;
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const renderer = new markedApi.Renderer();
|
|
59
|
+
renderer.html = function (html) {
|
|
60
|
+
return escapeHtml(html);
|
|
61
|
+
};
|
|
62
|
+
renderer.link = function (href, title, text) {
|
|
63
|
+
const safeHref = sanitizeMarkdownUrl(href);
|
|
64
|
+
if (!safeHref) {
|
|
65
|
+
return text || '';
|
|
66
|
+
}
|
|
67
|
+
let output = '<a href="' + escapeHtml(safeHref) + '" target="_blank" rel="noopener noreferrer"';
|
|
68
|
+
if (title) {
|
|
69
|
+
output += ' title="' + escapeHtml(title) + '"';
|
|
70
|
+
}
|
|
71
|
+
output += '>' + (text || '') + '</a>';
|
|
72
|
+
return output;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
markedApi.use({
|
|
76
|
+
gfm: true,
|
|
77
|
+
breaks: true,
|
|
78
|
+
renderer
|
|
79
|
+
});
|
|
80
|
+
runtime.available = true;
|
|
81
|
+
} catch (e) {
|
|
82
|
+
runtime.available = false;
|
|
83
|
+
}
|
|
84
|
+
runtime.configured = true;
|
|
85
|
+
return runtime.available;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function shouldRenderMessage(msg) {
|
|
89
|
+
return Boolean(msg && msg.mode === 'agent' && msg.role === 'assistant');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function render(content) {
|
|
93
|
+
const source = String(content == null ? '' : content);
|
|
94
|
+
if (!source) {
|
|
95
|
+
return '';
|
|
96
|
+
}
|
|
97
|
+
if (!ensureRendererConfigured()) {
|
|
98
|
+
return '';
|
|
99
|
+
}
|
|
100
|
+
const markedApi = getMarkedApi();
|
|
101
|
+
if (!markedApi) {
|
|
102
|
+
return '';
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
return String(markedApi.parse(source) || '');
|
|
106
|
+
} catch (e) {
|
|
107
|
+
return '';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
window.ManyoyoMarkdown = {
|
|
112
|
+
shouldRenderMessage,
|
|
113
|
+
render
|
|
114
|
+
};
|
|
115
|
+
}());
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
.bubble .md-content {
|
|
2
|
+
color: var(--text);
|
|
3
|
+
font-size: 13px;
|
|
4
|
+
line-height: 1.6;
|
|
5
|
+
word-break: break-word;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.bubble .md-content > :first-child {
|
|
9
|
+
margin-top: 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.bubble .md-content > :last-child {
|
|
13
|
+
margin-bottom: 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.bubble .md-content p,
|
|
17
|
+
.bubble .md-content ul,
|
|
18
|
+
.bubble .md-content ol,
|
|
19
|
+
.bubble .md-content blockquote,
|
|
20
|
+
.bubble .md-content pre,
|
|
21
|
+
.bubble .md-content table {
|
|
22
|
+
margin: 0.6em 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.bubble .md-content ul,
|
|
26
|
+
.bubble .md-content ol {
|
|
27
|
+
padding-left: 1.4em;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.bubble .md-content code {
|
|
31
|
+
font-family: var(--font-mono);
|
|
32
|
+
font-size: 12px;
|
|
33
|
+
padding: 1px 5px;
|
|
34
|
+
border-radius: 6px;
|
|
35
|
+
background: rgba(194, 149, 79, 0.16);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.bubble .md-content pre {
|
|
39
|
+
border-radius: 8px;
|
|
40
|
+
padding: 9px 10px;
|
|
41
|
+
border: 1px solid #e6d2b7;
|
|
42
|
+
background: rgba(255, 244, 224, 0.72);
|
|
43
|
+
overflow-x: auto;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.bubble .md-content pre code {
|
|
47
|
+
background: transparent;
|
|
48
|
+
border-radius: 0;
|
|
49
|
+
padding: 0;
|
|
50
|
+
font-size: 12px;
|
|
51
|
+
line-height: 1.5;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.bubble .md-content blockquote {
|
|
55
|
+
margin-left: 0;
|
|
56
|
+
padding-left: 10px;
|
|
57
|
+
border-left: 3px solid #dcb788;
|
|
58
|
+
color: #66492d;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.bubble .md-content table {
|
|
62
|
+
border-collapse: collapse;
|
|
63
|
+
width: 100%;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.bubble .md-content th,
|
|
67
|
+
.bubble .md-content td {
|
|
68
|
+
border: 1px solid #e6d2b7;
|
|
69
|
+
padding: 4px 6px;
|
|
70
|
+
text-align: left;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.bubble .md-content a {
|
|
74
|
+
color: #8a4f17;
|
|
75
|
+
text-decoration-color: rgba(138, 79, 23, 0.45);
|
|
76
|
+
}
|
package/lib/web/server.js
CHANGED
|
@@ -9,7 +9,11 @@ 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 {
|
|
12
|
+
const {
|
|
13
|
+
resolveAgentProgram,
|
|
14
|
+
resolveAgentPromptCommandTemplate,
|
|
15
|
+
buildAgentResumeCommand
|
|
16
|
+
} = require('../agent-resume');
|
|
13
17
|
|
|
14
18
|
const WEB_HISTORY_MAX_MESSAGES = 500;
|
|
15
19
|
const WEB_OUTPUT_MAX_CHARS = 16000;
|
|
@@ -19,6 +23,9 @@ const WEB_TERMINAL_DEFAULT_COLS = 120;
|
|
|
19
23
|
const WEB_TERMINAL_DEFAULT_ROWS = 36;
|
|
20
24
|
const WEB_TERMINAL_MIN_COLS = 40;
|
|
21
25
|
const WEB_TERMINAL_MIN_ROWS = 12;
|
|
26
|
+
const WEB_AGENT_CONTEXT_MAX_MESSAGES = 24;
|
|
27
|
+
const WEB_AGENT_CONTEXT_MAX_CHARS = 6000;
|
|
28
|
+
const WEB_AGENT_CONTEXT_PER_MESSAGE_MAX_CHARS = 600;
|
|
22
29
|
const WEB_AUTH_COOKIE_NAME = 'manyoyo_web_auth';
|
|
23
30
|
const WEB_AUTH_TTL_SECONDS = 12 * 60 * 60;
|
|
24
31
|
const FRONTEND_DIR = path.join(__dirname, 'frontend');
|
|
@@ -61,6 +68,7 @@ const DEFAULT_WEB_CONFIG_TEMPLATE = `{
|
|
|
61
68
|
let XTERM_JS_FILE = null;
|
|
62
69
|
let XTERM_CSS_FILE = null;
|
|
63
70
|
let XTERM_ADDON_FIT_JS_FILE = null;
|
|
71
|
+
let MARKED_MIN_JS_FILE = null;
|
|
64
72
|
try {
|
|
65
73
|
const xtermPackageDir = path.dirname(require.resolve('@xterm/xterm/package.json'));
|
|
66
74
|
XTERM_JS_FILE = path.join(xtermPackageDir, 'lib', 'xterm.js');
|
|
@@ -75,6 +83,11 @@ try {
|
|
|
75
83
|
} catch (e) {
|
|
76
84
|
XTERM_ADDON_FIT_JS_FILE = null;
|
|
77
85
|
}
|
|
86
|
+
try {
|
|
87
|
+
MARKED_MIN_JS_FILE = require.resolve('marked/marked.min.js');
|
|
88
|
+
} catch (e) {
|
|
89
|
+
MARKED_MIN_JS_FILE = null;
|
|
90
|
+
}
|
|
78
91
|
|
|
79
92
|
const MIME_TYPES = {
|
|
80
93
|
'.css': 'text/css; charset=utf-8',
|
|
@@ -106,7 +119,12 @@ function loadWebSessionHistory(webHistoryDir, containerName) {
|
|
|
106
119
|
containerName,
|
|
107
120
|
updatedAt: null,
|
|
108
121
|
messages: [],
|
|
109
|
-
agentPromptCommand: ''
|
|
122
|
+
agentPromptCommand: '',
|
|
123
|
+
agentProgram: '',
|
|
124
|
+
resumeSupported: false,
|
|
125
|
+
lastResumeAt: null,
|
|
126
|
+
lastResumeOk: null,
|
|
127
|
+
lastResumeError: ''
|
|
110
128
|
};
|
|
111
129
|
}
|
|
112
130
|
|
|
@@ -118,14 +136,24 @@ function loadWebSessionHistory(webHistoryDir, containerName) {
|
|
|
118
136
|
messages: Array.isArray(data.messages) ? data.messages : [],
|
|
119
137
|
agentPromptCommand: typeof data.agentPromptCommand === 'string'
|
|
120
138
|
? data.agentPromptCommand
|
|
121
|
-
: ''
|
|
139
|
+
: '',
|
|
140
|
+
agentProgram: typeof data.agentProgram === 'string' ? data.agentProgram : '',
|
|
141
|
+
resumeSupported: data.resumeSupported === true,
|
|
142
|
+
lastResumeAt: typeof data.lastResumeAt === 'string' ? data.lastResumeAt : null,
|
|
143
|
+
lastResumeOk: typeof data.lastResumeOk === 'boolean' ? data.lastResumeOk : null,
|
|
144
|
+
lastResumeError: typeof data.lastResumeError === 'string' ? data.lastResumeError : ''
|
|
122
145
|
};
|
|
123
146
|
} catch (e) {
|
|
124
147
|
return {
|
|
125
148
|
containerName,
|
|
126
149
|
updatedAt: null,
|
|
127
150
|
messages: [],
|
|
128
|
-
agentPromptCommand: ''
|
|
151
|
+
agentPromptCommand: '',
|
|
152
|
+
agentProgram: '',
|
|
153
|
+
resumeSupported: false,
|
|
154
|
+
lastResumeAt: null,
|
|
155
|
+
lastResumeOk: null,
|
|
156
|
+
lastResumeError: ''
|
|
129
157
|
};
|
|
130
158
|
}
|
|
131
159
|
}
|
|
@@ -174,7 +202,28 @@ function appendWebSessionMessage(webHistoryDir, containerName, role, content, ex
|
|
|
174
202
|
function setWebSessionAgentPromptCommand(webHistoryDir, containerName, agentPromptCommand) {
|
|
175
203
|
const history = loadWebSessionHistory(webHistoryDir, containerName);
|
|
176
204
|
history.agentPromptCommand = normalizeAgentPromptCommandTemplate(agentPromptCommand, 'agentPromptCommand');
|
|
205
|
+
const agentProgram = resolveAgentProgram(history.agentPromptCommand);
|
|
206
|
+
const resumeCommand = buildAgentResumeCommand(agentProgram);
|
|
207
|
+
history.agentProgram = agentProgram || '';
|
|
208
|
+
history.resumeSupported = Boolean(resumeCommand);
|
|
209
|
+
if (!history.resumeSupported) {
|
|
210
|
+
history.lastResumeAt = null;
|
|
211
|
+
history.lastResumeOk = null;
|
|
212
|
+
history.lastResumeError = '';
|
|
213
|
+
}
|
|
214
|
+
saveWebSessionHistory(webHistoryDir, containerName, history);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function patchWebSessionAgentState(webHistoryDir, containerName, patch) {
|
|
218
|
+
const history = loadWebSessionHistory(webHistoryDir, containerName);
|
|
219
|
+
if (!patch || typeof patch !== 'object') {
|
|
220
|
+
return history;
|
|
221
|
+
}
|
|
222
|
+
Object.keys(patch).forEach(key => {
|
|
223
|
+
history[key] = patch[key];
|
|
224
|
+
});
|
|
177
225
|
saveWebSessionHistory(webHistoryDir, containerName, history);
|
|
226
|
+
return history;
|
|
178
227
|
}
|
|
179
228
|
|
|
180
229
|
function stripAnsi(text) {
|
|
@@ -220,6 +269,72 @@ function renderAgentPromptCommand(template, prompt) {
|
|
|
220
269
|
return templateText.replace(/\{prompt\}/g, safePrompt);
|
|
221
270
|
}
|
|
222
271
|
|
|
272
|
+
function getAgentRuntimeMeta(history) {
|
|
273
|
+
const sessionHistory = history && typeof history === 'object' ? history : {};
|
|
274
|
+
const template = normalizeAgentPromptCommandTemplate(sessionHistory.agentPromptCommand, 'agentPromptCommand');
|
|
275
|
+
const agentProgram = resolveAgentProgram(template);
|
|
276
|
+
const resumeCommand = buildAgentResumeCommand(agentProgram);
|
|
277
|
+
return {
|
|
278
|
+
agentProgram: agentProgram || '',
|
|
279
|
+
resumeCommand: resumeCommand || '',
|
|
280
|
+
resumeSupported: Boolean(resumeCommand)
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function hasAgentConversationHistory(history) {
|
|
285
|
+
const messages = history && Array.isArray(history.messages) ? history.messages : [];
|
|
286
|
+
for (const message of messages) {
|
|
287
|
+
if (!message || typeof message !== 'object') continue;
|
|
288
|
+
if (message.mode !== 'agent') continue;
|
|
289
|
+
if (message.role === 'user' || message.role === 'assistant') {
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function clipAgentContextMessageText(text) {
|
|
297
|
+
const raw = clipText(stripAnsi(String(text || '')), WEB_AGENT_CONTEXT_PER_MESSAGE_MAX_CHARS);
|
|
298
|
+
return raw
|
|
299
|
+
.replace(/\r/g, '')
|
|
300
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
301
|
+
.trim();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function buildAgentPromptWithHistory(history, prompt) {
|
|
305
|
+
const sessionHistory = history && Array.isArray(history.messages) ? history.messages : [];
|
|
306
|
+
const relevantMessages = sessionHistory
|
|
307
|
+
.filter(message => message && message.mode === 'agent' && (message.role === 'user' || message.role === 'assistant'))
|
|
308
|
+
.slice(-WEB_AGENT_CONTEXT_MAX_MESSAGES);
|
|
309
|
+
if (!relevantMessages.length) {
|
|
310
|
+
return String(prompt || '');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const lines = [];
|
|
314
|
+
for (const message of relevantMessages) {
|
|
315
|
+
const roleName = message.role === 'user' ? '用户' : '助手';
|
|
316
|
+
const content = clipAgentContextMessageText(message.content);
|
|
317
|
+
if (!content) continue;
|
|
318
|
+
lines.push(`${roleName}: ${content}`);
|
|
319
|
+
}
|
|
320
|
+
if (!lines.length) {
|
|
321
|
+
return String(prompt || '');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
let historyText = lines.join('\n\n');
|
|
325
|
+
if (historyText.length > WEB_AGENT_CONTEXT_MAX_CHARS) {
|
|
326
|
+
historyText = historyText.slice(historyText.length - WEB_AGENT_CONTEXT_MAX_CHARS);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return [
|
|
330
|
+
'以下是当前会话最近对话历史(按时间顺序):',
|
|
331
|
+
historyText,
|
|
332
|
+
'---',
|
|
333
|
+
'请基于以上历史回答当前问题。',
|
|
334
|
+
`当前问题: ${String(prompt || '').trim()}`
|
|
335
|
+
].join('\n');
|
|
336
|
+
}
|
|
337
|
+
|
|
223
338
|
function secureStringEqual(a, b) {
|
|
224
339
|
const aStr = String(a || '');
|
|
225
340
|
const bStr = String(b || '');
|
|
@@ -745,6 +860,8 @@ function buildCreateRuntime(ctx, state, payload) {
|
|
|
745
860
|
'agentPromptCommand'
|
|
746
861
|
);
|
|
747
862
|
const agentPromptCommand = configuredAgentPromptCommand || inferredAgentPromptCommand;
|
|
863
|
+
const agentProgram = resolveAgentProgram(agentPromptCommand);
|
|
864
|
+
const resumeSupported = Boolean(buildAgentResumeCommand(agentProgram));
|
|
748
865
|
|
|
749
866
|
let containerEnvs = Array.isArray(ctx.containerEnvs) ? ctx.containerEnvs.slice() : [];
|
|
750
867
|
if (hasRequestEnv || hasRequestEnvFile || hasConfigEnv || hasConfigEnvFile) {
|
|
@@ -813,6 +930,8 @@ function buildCreateRuntime(ctx, state, payload) {
|
|
|
813
930
|
shell: shell || '',
|
|
814
931
|
shellSuffix: shellSuffix || '',
|
|
815
932
|
agentEnabled: isAgentPromptCommandEnabled(agentPromptCommand),
|
|
933
|
+
agentProgram: agentProgram || '',
|
|
934
|
+
resumeSupported,
|
|
816
935
|
yolo: yolo || '',
|
|
817
936
|
envCount: Math.floor(containerEnvs.length / 2),
|
|
818
937
|
volumeCount: Math.floor(containerVolumes.length / 2),
|
|
@@ -995,6 +1114,7 @@ function getValidSessionName(ctx, res, encodedName) {
|
|
|
995
1114
|
|
|
996
1115
|
function buildSessionSummary(ctx, state, containerMap, name) {
|
|
997
1116
|
const history = loadWebSessionHistory(state.webHistoryDir, name);
|
|
1117
|
+
const agentMeta = getAgentRuntimeMeta(history);
|
|
998
1118
|
const latestMessage = history.messages.length ? history.messages[history.messages.length - 1] : null;
|
|
999
1119
|
const containerInfo = containerMap[name] || {};
|
|
1000
1120
|
const updatedAt = history.updatedAt || (latestMessage && latestMessage.timestamp) || null;
|
|
@@ -1004,7 +1124,9 @@ function buildSessionSummary(ctx, state, containerMap, name) {
|
|
|
1004
1124
|
image: containerInfo.image || '',
|
|
1005
1125
|
updatedAt,
|
|
1006
1126
|
messageCount: history.messages.length,
|
|
1007
|
-
agentEnabled: isAgentPromptCommandEnabled(history.agentPromptCommand)
|
|
1127
|
+
agentEnabled: isAgentPromptCommandEnabled(history.agentPromptCommand),
|
|
1128
|
+
agentProgram: agentMeta.agentProgram,
|
|
1129
|
+
resumeSupported: agentMeta.resumeSupported
|
|
1008
1130
|
};
|
|
1009
1131
|
}
|
|
1010
1132
|
|
|
@@ -1033,6 +1155,9 @@ function resolveVendorAsset(name) {
|
|
|
1033
1155
|
if (name === 'xterm-addon-fit.js') {
|
|
1034
1156
|
return XTERM_ADDON_FIT_JS_FILE && fs.existsSync(XTERM_ADDON_FIT_JS_FILE) ? XTERM_ADDON_FIT_JS_FILE : null;
|
|
1035
1157
|
}
|
|
1158
|
+
if (name === 'marked.min.js') {
|
|
1159
|
+
return MARKED_MIN_JS_FILE && fs.existsSync(MARKED_MIN_JS_FILE) ? MARKED_MIN_JS_FILE : null;
|
|
1160
|
+
}
|
|
1036
1161
|
return null;
|
|
1037
1162
|
}
|
|
1038
1163
|
|
|
@@ -1478,18 +1603,59 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
1478
1603
|
return;
|
|
1479
1604
|
}
|
|
1480
1605
|
|
|
1481
|
-
const command = renderAgentPromptCommand(history.agentPromptCommand, prompt);
|
|
1482
1606
|
await ensureWebContainer(ctx, state, containerName);
|
|
1483
|
-
|
|
1607
|
+
const agentMeta = getAgentRuntimeMeta(history);
|
|
1608
|
+
const hasPriorConversation = hasAgentConversationHistory(history);
|
|
1609
|
+
let resumeAttempted = false;
|
|
1610
|
+
let resumeSucceeded = false;
|
|
1611
|
+
let resumeError = '';
|
|
1612
|
+
if (hasPriorConversation && agentMeta.resumeSupported && agentMeta.resumeCommand) {
|
|
1613
|
+
resumeAttempted = true;
|
|
1614
|
+
const resumeResult = await execCommandInWebContainer(ctx, containerName, agentMeta.resumeCommand);
|
|
1615
|
+
if (resumeResult.exitCode === 0) {
|
|
1616
|
+
resumeSucceeded = true;
|
|
1617
|
+
} else {
|
|
1618
|
+
resumeError = clipText(String(resumeResult.output || '(无输出)'), 1200);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
const effectivePrompt = resumeSucceeded
|
|
1623
|
+
? prompt
|
|
1624
|
+
: buildAgentPromptWithHistory(history, prompt);
|
|
1625
|
+
const command = renderAgentPromptCommand(history.agentPromptCommand, effectivePrompt);
|
|
1626
|
+
const contextMode = resumeSucceeded ? 'resume' : (hasPriorConversation ? 'history-injected' : 'first-turn');
|
|
1627
|
+
appendWebSessionMessage(state.webHistoryDir, containerName, 'user', prompt, {
|
|
1628
|
+
mode: 'agent',
|
|
1629
|
+
contextMode
|
|
1630
|
+
});
|
|
1484
1631
|
const result = await execCommandInWebContainer(ctx, containerName, command);
|
|
1485
1632
|
appendWebSessionMessage(
|
|
1486
1633
|
state.webHistoryDir,
|
|
1487
1634
|
containerName,
|
|
1488
1635
|
'assistant',
|
|
1489
1636
|
result.output,
|
|
1490
|
-
{
|
|
1637
|
+
{
|
|
1638
|
+
exitCode: result.exitCode,
|
|
1639
|
+
mode: 'agent',
|
|
1640
|
+
contextMode,
|
|
1641
|
+
resumeAttempted,
|
|
1642
|
+
resumeSucceeded
|
|
1643
|
+
}
|
|
1491
1644
|
);
|
|
1492
|
-
|
|
1645
|
+
patchWebSessionAgentState(state.webHistoryDir, containerName, {
|
|
1646
|
+
agentProgram: agentMeta.agentProgram,
|
|
1647
|
+
resumeSupported: agentMeta.resumeSupported,
|
|
1648
|
+
lastResumeAt: resumeAttempted ? new Date().toISOString() : history.lastResumeAt || null,
|
|
1649
|
+
lastResumeOk: resumeAttempted ? resumeSucceeded : history.lastResumeOk,
|
|
1650
|
+
lastResumeError: resumeAttempted ? (resumeSucceeded ? '' : resumeError) : history.lastResumeError || ''
|
|
1651
|
+
});
|
|
1652
|
+
sendJson(res, 200, {
|
|
1653
|
+
exitCode: result.exitCode,
|
|
1654
|
+
output: result.output,
|
|
1655
|
+
contextMode,
|
|
1656
|
+
resumeAttempted,
|
|
1657
|
+
resumeSucceeded
|
|
1658
|
+
});
|
|
1493
1659
|
}
|
|
1494
1660
|
},
|
|
1495
1661
|
{
|
|
@@ -1629,7 +1795,7 @@ async function startWebServer(options) {
|
|
|
1629
1795
|
const appFrontendMatch = pathname.match(/^\/app\/frontend\/([A-Za-z0-9._-]+)$/);
|
|
1630
1796
|
if (req.method === 'GET' && appFrontendMatch) {
|
|
1631
1797
|
const assetName = appFrontendMatch[1];
|
|
1632
|
-
if (!(assetName === 'app.css' || assetName === 'app.js')) {
|
|
1798
|
+
if (!(assetName === 'app.css' || assetName === 'app.js' || assetName === 'markdown.css' || assetName === 'markdown-renderer.js')) {
|
|
1633
1799
|
sendHtml(res, 404, '<h1>404 Not Found</h1>');
|
|
1634
1800
|
return;
|
|
1635
1801
|
}
|
|
@@ -1640,7 +1806,7 @@ async function startWebServer(options) {
|
|
|
1640
1806
|
const appVendorMatch = pathname.match(/^\/app\/vendor\/([A-Za-z0-9._-]+)$/);
|
|
1641
1807
|
if (req.method === 'GET' && appVendorMatch) {
|
|
1642
1808
|
const assetName = appVendorMatch[1];
|
|
1643
|
-
if (!(assetName === 'xterm.css' || assetName === 'xterm.js' || assetName === 'xterm-addon-fit.js')) {
|
|
1809
|
+
if (!(assetName === 'xterm.css' || assetName === 'xterm.js' || assetName === 'xterm-addon-fit.js' || assetName === 'marked.min.js')) {
|
|
1644
1810
|
sendHtml(res, 404, '<h1>404 Not Found</h1>');
|
|
1645
1811
|
return;
|
|
1646
1812
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xcanwin/manyoyo",
|
|
3
|
-
"version": "5.2.
|
|
3
|
+
"version": "5.2.5",
|
|
4
4
|
"imageVersion": "1.8.1-common",
|
|
5
5
|
"description": "AI Agent CLI Security Sandbox for Docker and Podman",
|
|
6
6
|
"keywords": [
|
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
"@xterm/xterm": "^6.0.0",
|
|
59
59
|
"commander": "^12.0.0",
|
|
60
60
|
"json5": "^2.2.3",
|
|
61
|
+
"marked": "^12.0.2",
|
|
61
62
|
"playwright": "1.58.2",
|
|
62
63
|
"ws": "^8.19.0"
|
|
63
64
|
},
|