@xcanwin/manyoyo 5.2.0 → 5.2.8
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 +1 -1
- package/lib/web/frontend/app.css +33 -12
- package/lib/web/frontend/app.html +5 -2
- package/lib/web/frontend/app.js +58 -9
- package/lib/web/frontend/markdown-renderer.js +143 -0
- package/lib/web/frontend/markdown.css +76 -0
- package/lib/web/server.js +211 -13
- package/package.json +2 -1
package/bin/manyoyo.js
CHANGED
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
|
? '输入提示词,例如:请帮我分析当前项目结构并给出重构建议'
|
|
@@ -1003,7 +1014,7 @@
|
|
|
1003
1014
|
|
|
1004
1015
|
async function api(url, options) {
|
|
1005
1016
|
const requestOptions = Object.assign(
|
|
1006
|
-
{ headers: { 'Content-Type': 'application/json' } },
|
|
1017
|
+
{ headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } },
|
|
1007
1018
|
options || {}
|
|
1008
1019
|
);
|
|
1009
1020
|
const response = await fetch(url, requestOptions);
|
|
@@ -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,143 @@
|
|
|
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 escapeHtml(text || '');
|
|
66
|
+
}
|
|
67
|
+
// [P1-02] 移除 marked 已渲染链接文本中的 on* 事件属性,防止内联 HTML 注入 XSS
|
|
68
|
+
const safeText = String(text || '').replace(/\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]*)/gi, '');
|
|
69
|
+
let output = '<a href="' + escapeHtml(safeHref) + '" target="_blank" rel="noopener noreferrer"';
|
|
70
|
+
if (title) {
|
|
71
|
+
output += ' title="' + escapeHtml(title) + '"';
|
|
72
|
+
}
|
|
73
|
+
output += '>' + safeText + '</a>';
|
|
74
|
+
return output;
|
|
75
|
+
};
|
|
76
|
+
// [P1-01] 重写 image 渲染器:
|
|
77
|
+
// - 外部 http/https 图片转为可点击链接,避免浏览器自动发起外部请求(追踪像素风险)
|
|
78
|
+
// - 相对路径图片正常渲染为 <img>
|
|
79
|
+
// - 危险协议(javascript:/data: 等)降级为纯文本
|
|
80
|
+
renderer.image = function (href, title, text) {
|
|
81
|
+
const safeHref = sanitizeMarkdownUrl(href);
|
|
82
|
+
if (!safeHref) {
|
|
83
|
+
return escapeHtml(text || '');
|
|
84
|
+
}
|
|
85
|
+
// 外部绝对 URL:转为链接,用户主动决定是否访问
|
|
86
|
+
if (/^https?:/i.test(safeHref)) {
|
|
87
|
+
const safeText = String(text || '').replace(/\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]*)/gi, '')
|
|
88
|
+
|| escapeHtml(safeHref);
|
|
89
|
+
let output = '<a href="' + escapeHtml(safeHref) + '" target="_blank" rel="noopener noreferrer"';
|
|
90
|
+
if (title) {
|
|
91
|
+
output += ' title="' + escapeHtml(title) + '"';
|
|
92
|
+
}
|
|
93
|
+
return output + '>[\uD83D\uDDBC\uFE0F点击查看图片:' + safeText + ']</a>';
|
|
94
|
+
}
|
|
95
|
+
// 相对路径:正常渲染为图片
|
|
96
|
+
let output = '<img src="' + escapeHtml(safeHref) + '" alt="' + escapeHtml(text || '') + '"';
|
|
97
|
+
if (title) {
|
|
98
|
+
output += ' title="' + escapeHtml(title) + '"';
|
|
99
|
+
}
|
|
100
|
+
return output + '>';
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
markedApi.use({
|
|
104
|
+
gfm: true,
|
|
105
|
+
breaks: true,
|
|
106
|
+
renderer
|
|
107
|
+
});
|
|
108
|
+
runtime.available = true;
|
|
109
|
+
} catch (e) {
|
|
110
|
+
runtime.available = false;
|
|
111
|
+
}
|
|
112
|
+
runtime.configured = true;
|
|
113
|
+
return runtime.available;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function shouldRenderMessage(msg) {
|
|
117
|
+
return Boolean(msg && msg.mode === 'agent' && msg.role === 'assistant');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function render(content) {
|
|
121
|
+
const source = String(content == null ? '' : content);
|
|
122
|
+
if (!source) {
|
|
123
|
+
return '';
|
|
124
|
+
}
|
|
125
|
+
if (!ensureRendererConfigured()) {
|
|
126
|
+
return '';
|
|
127
|
+
}
|
|
128
|
+
const markedApi = getMarkedApi();
|
|
129
|
+
if (!markedApi) {
|
|
130
|
+
return '';
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
return String(markedApi.parse(source) || '');
|
|
134
|
+
} catch (e) {
|
|
135
|
+
return '';
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
window.ManyoyoMarkdown = {
|
|
140
|
+
shouldRenderMessage,
|
|
141
|
+
render
|
|
142
|
+
};
|
|
143
|
+
}());
|
|
@@ -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,9 +202,30 @@ 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
|
+
}
|
|
177
214
|
saveWebSessionHistory(webHistoryDir, containerName, history);
|
|
178
215
|
}
|
|
179
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
|
+
});
|
|
225
|
+
saveWebSessionHistory(webHistoryDir, containerName, history);
|
|
226
|
+
return history;
|
|
227
|
+
}
|
|
228
|
+
|
|
180
229
|
function stripAnsi(text) {
|
|
181
230
|
if (typeof text !== 'string') return '';
|
|
182
231
|
return text.replace(/\x1b\[[0-9;]*m/g, '');
|
|
@@ -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 || '');
|
|
@@ -299,11 +414,11 @@ function clearWebAuthSession(state, req) {
|
|
|
299
414
|
}
|
|
300
415
|
|
|
301
416
|
function getWebAuthCookie(sessionId) {
|
|
302
|
-
return `${WEB_AUTH_COOKIE_NAME}=${encodeURIComponent(sessionId)}; Path=/; HttpOnly; SameSite=
|
|
417
|
+
return `${WEB_AUTH_COOKIE_NAME}=${encodeURIComponent(sessionId)}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${WEB_AUTH_TTL_SECONDS}`;
|
|
303
418
|
}
|
|
304
419
|
|
|
305
420
|
function getWebAuthClearCookie() {
|
|
306
|
-
return `${WEB_AUTH_COOKIE_NAME}=; Path=/; HttpOnly; SameSite=
|
|
421
|
+
return `${WEB_AUTH_COOKIE_NAME}=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0`;
|
|
307
422
|
}
|
|
308
423
|
|
|
309
424
|
function getDefaultWebConfigPath() {
|
|
@@ -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
|
|
|
@@ -1330,6 +1455,14 @@ function sendWebUnauthorized(res, pathname) {
|
|
|
1330
1455
|
}
|
|
1331
1456
|
|
|
1332
1457
|
async function handleWebApi(req, res, pathname, ctx, state) {
|
|
1458
|
+
// [P2-03] 对非只读请求校验自定义头,防止 CSRF 攻击
|
|
1459
|
+
// 跨站请求无法设置自定义头(浏览器同源策略),合法前端请求统一携带此头
|
|
1460
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
1461
|
+
if (req.headers['x-requested-with'] !== 'XMLHttpRequest') {
|
|
1462
|
+
sendJson(res, 403, { error: 'CSRF check failed' });
|
|
1463
|
+
return true;
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1333
1466
|
const routes = [
|
|
1334
1467
|
{
|
|
1335
1468
|
method: 'GET',
|
|
@@ -1478,18 +1611,59 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
1478
1611
|
return;
|
|
1479
1612
|
}
|
|
1480
1613
|
|
|
1481
|
-
const command = renderAgentPromptCommand(history.agentPromptCommand, prompt);
|
|
1482
1614
|
await ensureWebContainer(ctx, state, containerName);
|
|
1483
|
-
|
|
1615
|
+
const agentMeta = getAgentRuntimeMeta(history);
|
|
1616
|
+
const hasPriorConversation = hasAgentConversationHistory(history);
|
|
1617
|
+
let resumeAttempted = false;
|
|
1618
|
+
let resumeSucceeded = false;
|
|
1619
|
+
let resumeError = '';
|
|
1620
|
+
if (hasPriorConversation && agentMeta.resumeSupported && agentMeta.resumeCommand) {
|
|
1621
|
+
resumeAttempted = true;
|
|
1622
|
+
const resumeResult = await execCommandInWebContainer(ctx, containerName, agentMeta.resumeCommand);
|
|
1623
|
+
if (resumeResult.exitCode === 0) {
|
|
1624
|
+
resumeSucceeded = true;
|
|
1625
|
+
} else {
|
|
1626
|
+
resumeError = clipText(String(resumeResult.output || '(无输出)'), 1200);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
const effectivePrompt = resumeSucceeded
|
|
1631
|
+
? prompt
|
|
1632
|
+
: buildAgentPromptWithHistory(history, prompt);
|
|
1633
|
+
const command = renderAgentPromptCommand(history.agentPromptCommand, effectivePrompt);
|
|
1634
|
+
const contextMode = resumeSucceeded ? 'resume' : (hasPriorConversation ? 'history-injected' : 'first-turn');
|
|
1635
|
+
appendWebSessionMessage(state.webHistoryDir, containerName, 'user', prompt, {
|
|
1636
|
+
mode: 'agent',
|
|
1637
|
+
contextMode
|
|
1638
|
+
});
|
|
1484
1639
|
const result = await execCommandInWebContainer(ctx, containerName, command);
|
|
1485
1640
|
appendWebSessionMessage(
|
|
1486
1641
|
state.webHistoryDir,
|
|
1487
1642
|
containerName,
|
|
1488
1643
|
'assistant',
|
|
1489
1644
|
result.output,
|
|
1490
|
-
{
|
|
1645
|
+
{
|
|
1646
|
+
exitCode: result.exitCode,
|
|
1647
|
+
mode: 'agent',
|
|
1648
|
+
contextMode,
|
|
1649
|
+
resumeAttempted,
|
|
1650
|
+
resumeSucceeded
|
|
1651
|
+
}
|
|
1491
1652
|
);
|
|
1492
|
-
|
|
1653
|
+
patchWebSessionAgentState(state.webHistoryDir, containerName, {
|
|
1654
|
+
agentProgram: agentMeta.agentProgram,
|
|
1655
|
+
resumeSupported: agentMeta.resumeSupported,
|
|
1656
|
+
lastResumeAt: resumeAttempted ? new Date().toISOString() : history.lastResumeAt || null,
|
|
1657
|
+
lastResumeOk: resumeAttempted ? resumeSucceeded : history.lastResumeOk,
|
|
1658
|
+
lastResumeError: resumeAttempted ? (resumeSucceeded ? '' : resumeError) : history.lastResumeError || ''
|
|
1659
|
+
});
|
|
1660
|
+
sendJson(res, 200, {
|
|
1661
|
+
exitCode: result.exitCode,
|
|
1662
|
+
output: result.output,
|
|
1663
|
+
contextMode,
|
|
1664
|
+
resumeAttempted,
|
|
1665
|
+
resumeSucceeded
|
|
1666
|
+
});
|
|
1493
1667
|
}
|
|
1494
1668
|
},
|
|
1495
1669
|
{
|
|
@@ -1629,7 +1803,7 @@ async function startWebServer(options) {
|
|
|
1629
1803
|
const appFrontendMatch = pathname.match(/^\/app\/frontend\/([A-Za-z0-9._-]+)$/);
|
|
1630
1804
|
if (req.method === 'GET' && appFrontendMatch) {
|
|
1631
1805
|
const assetName = appFrontendMatch[1];
|
|
1632
|
-
if (!(assetName === 'app.css' || assetName === 'app.js')) {
|
|
1806
|
+
if (!(assetName === 'app.css' || assetName === 'app.js' || assetName === 'markdown.css' || assetName === 'markdown-renderer.js')) {
|
|
1633
1807
|
sendHtml(res, 404, '<h1>404 Not Found</h1>');
|
|
1634
1808
|
return;
|
|
1635
1809
|
}
|
|
@@ -1640,7 +1814,7 @@ async function startWebServer(options) {
|
|
|
1640
1814
|
const appVendorMatch = pathname.match(/^\/app\/vendor\/([A-Za-z0-9._-]+)$/);
|
|
1641
1815
|
if (req.method === 'GET' && appVendorMatch) {
|
|
1642
1816
|
const assetName = appVendorMatch[1];
|
|
1643
|
-
if (!(assetName === 'xterm.css' || assetName === 'xterm.js' || assetName === 'xterm-addon-fit.js')) {
|
|
1817
|
+
if (!(assetName === 'xterm.css' || assetName === 'xterm.js' || assetName === 'xterm-addon-fit.js' || assetName === 'marked.min.js')) {
|
|
1644
1818
|
sendHtml(res, 404, '<h1>404 Not Found</h1>');
|
|
1645
1819
|
return;
|
|
1646
1820
|
}
|
|
@@ -1687,6 +1861,30 @@ async function startWebServer(options) {
|
|
|
1687
1861
|
return;
|
|
1688
1862
|
}
|
|
1689
1863
|
|
|
1864
|
+
// [P1-03] Origin 校验,防止跨站 WebSocket 劫持(CSWSH)
|
|
1865
|
+
// 浏览器发起的 WebSocket 请求必须携带 Origin 头,非浏览器客户端(如 curl)不携带则放行
|
|
1866
|
+
const requestOrigin = req.headers.origin;
|
|
1867
|
+
if (requestOrigin) {
|
|
1868
|
+
const allowedOrigins = new Set();
|
|
1869
|
+
if (ctx.serverHost === '0.0.0.0') {
|
|
1870
|
+
// 0.0.0.0 监听时,以请求的 Host 头构造允许来源
|
|
1871
|
+
const hostHeader = req.headers.host || '';
|
|
1872
|
+
if (hostHeader) {
|
|
1873
|
+
allowedOrigins.add(`http://${hostHeader}`);
|
|
1874
|
+
allowedOrigins.add(`https://${hostHeader}`);
|
|
1875
|
+
}
|
|
1876
|
+
} else {
|
|
1877
|
+
allowedOrigins.add(`http://${formatUrlHost(ctx.serverHost)}:${listenPort}`);
|
|
1878
|
+
if (ctx.serverHost === '127.0.0.1') {
|
|
1879
|
+
allowedOrigins.add(`http://localhost:${listenPort}`);
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
if (allowedOrigins.size > 0 && !allowedOrigins.has(requestOrigin)) {
|
|
1883
|
+
sendWebSocketUpgradeError(socket, 403, 'Forbidden');
|
|
1884
|
+
return;
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1690
1888
|
const authSession = getWebAuthSession(state, req);
|
|
1691
1889
|
if (!authSession) {
|
|
1692
1890
|
sendWebSocketUpgradeError(socket, 401, 'UNAUTHORIZED');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xcanwin/manyoyo",
|
|
3
|
-
"version": "5.2.
|
|
3
|
+
"version": "5.2.8",
|
|
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
|
},
|