@xcanwin/manyoyo 6.2.6 → 6.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/lib/web/frontend/app.css +17 -0
- package/lib/web/frontend/app.js +12 -37
- package/lib/web/frontend/chat-behavior.js +59 -1
- package/lib/web/frontend/file-browser.js +77 -1
- package/lib/web/frontend/markdown-renderer.js +16 -4
- package/lib/web/frontend/markdown.css +21 -21
- package/lib/web/server.js +175 -27
- package/package.json +1 -1
package/lib/web/frontend/app.css
CHANGED
|
@@ -1224,6 +1224,23 @@ body.workspace-switcher-open .workspace-switcher-panel {
|
|
|
1224
1224
|
line-height: 1.6;
|
|
1225
1225
|
}
|
|
1226
1226
|
|
|
1227
|
+
.files-markdown-preview {
|
|
1228
|
+
padding: 4px 6px;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
.files-image-preview {
|
|
1232
|
+
display: flex;
|
|
1233
|
+
justify-content: center;
|
|
1234
|
+
padding: 12px;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
.files-image-preview img {
|
|
1238
|
+
max-width: 100%;
|
|
1239
|
+
height: auto;
|
|
1240
|
+
border-radius: 8px;
|
|
1241
|
+
border: 1px solid rgba(181, 146, 99, 0.2);
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1227
1244
|
.files-editor-host {
|
|
1228
1245
|
min-height: 100%;
|
|
1229
1246
|
}
|
package/lib/web/frontend/app.js
CHANGED
|
@@ -387,35 +387,6 @@
|
|
|
387
387
|
return status;
|
|
388
388
|
}
|
|
389
389
|
|
|
390
|
-
function buildStructuredTraceResidualLines(message) {
|
|
391
|
-
const lines = String(message && message.content ? message.content : '')
|
|
392
|
-
.split('\n')
|
|
393
|
-
.map(function (line) {
|
|
394
|
-
return String(line || '').trim();
|
|
395
|
-
})
|
|
396
|
-
.filter(Boolean);
|
|
397
|
-
const traceEvents = Array.isArray(message && message.traceEvents) ? message.traceEvents : [];
|
|
398
|
-
const consumed = new Map();
|
|
399
|
-
traceEvents.forEach(function (traceEvent) {
|
|
400
|
-
const key = traceEvent && traceEvent.text ? String(traceEvent.text).trim() : '';
|
|
401
|
-
if (!key) {
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
consumed.set(key, (consumed.get(key) || 0) + 1);
|
|
405
|
-
});
|
|
406
|
-
return lines.filter(function (line) {
|
|
407
|
-
if (!line || line === '[执行过程]') {
|
|
408
|
-
return false;
|
|
409
|
-
}
|
|
410
|
-
const remaining = consumed.get(line) || 0;
|
|
411
|
-
if (remaining > 0) {
|
|
412
|
-
consumed.set(line, remaining - 1);
|
|
413
|
-
return false;
|
|
414
|
-
}
|
|
415
|
-
return true;
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
|
|
419
390
|
function resolveTraceTone(traceEvent) {
|
|
420
391
|
const kind = traceEvent && traceEvent.kind ? String(traceEvent.kind) : '';
|
|
421
392
|
if (kind === 'command') return 'command';
|
|
@@ -586,20 +557,24 @@
|
|
|
586
557
|
const container = document.createElement('div');
|
|
587
558
|
container.className = 'trace-structured';
|
|
588
559
|
|
|
560
|
+
const messageId = message && message.id ? message.id : '';
|
|
561
|
+
const pending = Boolean(state.agentRun.active && state.agentRun.traceMessageId === messageId);
|
|
562
|
+
|
|
589
563
|
const flow = document.createElement('div');
|
|
590
564
|
flow.className = 'trace-flow';
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
565
|
+
if (pending) {
|
|
566
|
+
window.ManyoyoChatBehavior.buildStructuredTraceResidualLines(message).forEach(function (line) {
|
|
567
|
+
flow.appendChild(createResidualTraceCard(line));
|
|
568
|
+
});
|
|
569
|
+
}
|
|
594
570
|
const traceEvents = Array.isArray(message && message.traceEvents) ? message.traceEvents : [];
|
|
595
|
-
|
|
571
|
+
const mergedTraceEvents = window.ManyoyoChatBehavior.mergeToolTraceEvents(traceEvents);
|
|
572
|
+
mergedTraceEvents.forEach(function (traceEvent) {
|
|
596
573
|
flow.appendChild(createTraceEventCard(traceEvent));
|
|
597
574
|
});
|
|
598
575
|
|
|
599
|
-
const
|
|
600
|
-
const
|
|
601
|
-
const summaryInfo = window.ManyoyoChatBehavior.summarizeTraceFlow(traceEvents, { pending });
|
|
602
|
-
const hasError = traceEvents.some(function (event) {
|
|
576
|
+
const summaryInfo = window.ManyoyoChatBehavior.summarizeTraceFlow(mergedTraceEvents, { pending });
|
|
577
|
+
const hasError = mergedTraceEvents.some(function (event) {
|
|
603
578
|
return event && event.kind === 'error';
|
|
604
579
|
});
|
|
605
580
|
const defaultOpen = hasError;
|
|
@@ -22,6 +22,62 @@
|
|
|
22
22
|
return trimmed ? `${trimmed} · MANYOYO Web` : 'MANYOYO Web';
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
function buildStructuredTraceResidualLines(message) {
|
|
26
|
+
const lines = String(message && message.content ? message.content : '')
|
|
27
|
+
.split('\n')
|
|
28
|
+
.map(line => String(line || '').trim())
|
|
29
|
+
.filter(Boolean);
|
|
30
|
+
const traceEvents = Array.isArray(message && message.traceEvents) ? message.traceEvents : [];
|
|
31
|
+
const consumed = new Map();
|
|
32
|
+
traceEvents.forEach(traceEvent => {
|
|
33
|
+
const text = traceEvent && traceEvent.text ? String(traceEvent.text) : '';
|
|
34
|
+
if (!text) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
text.split('\n').forEach(subLine => {
|
|
38
|
+
const key = String(subLine || '').trim();
|
|
39
|
+
if (!key) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
consumed.set(key, (consumed.get(key) || 0) + 1);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
return lines.filter(line => {
|
|
46
|
+
if (!line || line === '[执行过程]') {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
const remaining = consumed.get(line) || 0;
|
|
50
|
+
if (remaining > 0) {
|
|
51
|
+
consumed.set(line, remaining - 1);
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
return true;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const MERGEABLE_TRACE_KINDS = new Set(['tool', 'command', 'mcp']);
|
|
59
|
+
|
|
60
|
+
function mergeToolTraceEvents(traceEvents) {
|
|
61
|
+
const events = Array.isArray(traceEvents) ? traceEvents : [];
|
|
62
|
+
const result = [];
|
|
63
|
+
const indexByKey = new Map();
|
|
64
|
+
events.forEach(event => {
|
|
65
|
+
const kind = event && event.kind ? String(event.kind) : '';
|
|
66
|
+
const toolId = event && event.toolId ? String(event.toolId) : '';
|
|
67
|
+
if (MERGEABLE_TRACE_KINDS.has(kind) && toolId) {
|
|
68
|
+
const key = `${kind}:${toolId}`;
|
|
69
|
+
if (indexByKey.has(key)) {
|
|
70
|
+
const index = indexByKey.get(key);
|
|
71
|
+
result[index] = Object.assign({}, result[index], event);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
indexByKey.set(key, result.length);
|
|
75
|
+
}
|
|
76
|
+
result.push(event);
|
|
77
|
+
});
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
|
|
25
81
|
function mergeTraceIntoReply(messages) {
|
|
26
82
|
const list = Array.isArray(messages) ? messages : [];
|
|
27
83
|
const result = [];
|
|
@@ -44,6 +100,8 @@
|
|
|
44
100
|
isNearBottom,
|
|
45
101
|
summarizeTraceFlow,
|
|
46
102
|
buildDocumentTitle,
|
|
47
|
-
mergeTraceIntoReply
|
|
103
|
+
mergeTraceIntoReply,
|
|
104
|
+
mergeToolTraceEvents,
|
|
105
|
+
buildStructuredTraceResidualLines
|
|
48
106
|
};
|
|
49
107
|
}());
|
|
@@ -104,6 +104,7 @@
|
|
|
104
104
|
<div class="files-preview-meta" data-role="preview-meta">请选择左侧文件或目录</div>
|
|
105
105
|
</div>
|
|
106
106
|
<div class="files-preview-actions">
|
|
107
|
+
<button type="button" class="secondary" data-action="toggle-markdown-view" hidden>查看源码</button>
|
|
107
108
|
<button type="button" class="secondary" data-action="save" disabled>保存</button>
|
|
108
109
|
</div>
|
|
109
110
|
</header>
|
|
@@ -122,6 +123,7 @@
|
|
|
122
123
|
const visitBtn = root.querySelector('[data-action="visit"]');
|
|
123
124
|
const mkdirBtn = root.querySelector('[data-action="mkdir"]');
|
|
124
125
|
const saveBtn = root.querySelector('[data-action="save"]');
|
|
126
|
+
const toggleMarkdownViewBtn = root.querySelector('[data-action="toggle-markdown-view"]');
|
|
125
127
|
|
|
126
128
|
const state = {
|
|
127
129
|
visible: false,
|
|
@@ -144,7 +146,8 @@
|
|
|
144
146
|
editor: null,
|
|
145
147
|
editorHost: null,
|
|
146
148
|
previewReadOnly: true,
|
|
147
|
-
previewDirty: false
|
|
149
|
+
previewDirty: false,
|
|
150
|
+
markdownViewMode: 'rendered'
|
|
148
151
|
};
|
|
149
152
|
|
|
150
153
|
function setStatus(text) {
|
|
@@ -161,6 +164,12 @@
|
|
|
161
164
|
state.editorHost = null;
|
|
162
165
|
}
|
|
163
166
|
|
|
167
|
+
function getPreviewLanguage() {
|
|
168
|
+
return state.selectedFile
|
|
169
|
+
? (state.selectedFile.language || inferLanguageFromPath(state.selectedFile.path))
|
|
170
|
+
: '';
|
|
171
|
+
}
|
|
172
|
+
|
|
164
173
|
function isEditablePreview() {
|
|
165
174
|
return Boolean(
|
|
166
175
|
state.selectedFile
|
|
@@ -168,6 +177,7 @@
|
|
|
168
177
|
&& state.previewReadOnly === false
|
|
169
178
|
&& state.historyOnly !== true
|
|
170
179
|
&& state.savingFile !== true
|
|
180
|
+
&& (getPreviewLanguage() !== 'markdown' || state.markdownViewMode === 'source')
|
|
171
181
|
);
|
|
172
182
|
}
|
|
173
183
|
|
|
@@ -179,6 +189,28 @@
|
|
|
179
189
|
saveBtn.textContent = state.savingFile ? '保存中...' : '保存';
|
|
180
190
|
}
|
|
181
191
|
|
|
192
|
+
function syncMarkdownToggleButton() {
|
|
193
|
+
if (!toggleMarkdownViewBtn) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const isMarkdown = Boolean(state.selectedFile)
|
|
197
|
+
&& state.selectedFile.kind === 'text'
|
|
198
|
+
&& getPreviewLanguage() === 'markdown';
|
|
199
|
+
toggleMarkdownViewBtn.hidden = !isMarkdown;
|
|
200
|
+
toggleMarkdownViewBtn.textContent = state.markdownViewMode === 'source' ? '查看渲染' : '查看源码';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function resolveMarkdownImageUrl(basePath, relativeHref) {
|
|
204
|
+
try {
|
|
205
|
+
const lastSlash = String(basePath || '').lastIndexOf('/');
|
|
206
|
+
const baseDir = lastSlash >= 0 ? basePath.slice(0, lastSlash + 1) : '/';
|
|
207
|
+
const resolvedPath = new URL(relativeHref, 'http://manyoyo-internal' + baseDir).pathname;
|
|
208
|
+
return '/api/sessions/' + encodeURIComponent(state.sessionName) + '/fs/raw?path=' + encodeURIComponent(resolvedPath);
|
|
209
|
+
} catch (e) {
|
|
210
|
+
return '';
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
182
214
|
function renderPreviewEmpty(title, description) {
|
|
183
215
|
state.selectedFile = null;
|
|
184
216
|
state.previewReadOnly = true;
|
|
@@ -193,6 +225,7 @@
|
|
|
193
225
|
previewBodyNode.innerHTML = `<div class="files-empty">${escapeHtml(description)}</div>`;
|
|
194
226
|
}
|
|
195
227
|
destroyEditor();
|
|
228
|
+
syncMarkdownToggleButton();
|
|
196
229
|
syncSaveButton();
|
|
197
230
|
}
|
|
198
231
|
|
|
@@ -226,12 +259,34 @@
|
|
|
226
259
|
previewMetaNode.textContent = `${payload.kind === 'text' ? '文本文件' : '文件'} · ${formatBytes(payload.size)} · ${modeLabel}${payload.truncated ? ' · 已截断预览' : ''}`;
|
|
227
260
|
}
|
|
228
261
|
if (!previewBodyNode) {
|
|
262
|
+
syncMarkdownToggleButton();
|
|
229
263
|
syncSaveButton();
|
|
230
264
|
return;
|
|
231
265
|
}
|
|
232
266
|
|
|
233
267
|
if (payload.kind === 'text') {
|
|
234
268
|
const language = payload.language || inferLanguageFromPath(payload.path);
|
|
269
|
+
const showMarkdownRendered = language === 'markdown'
|
|
270
|
+
&& state.markdownViewMode !== 'source'
|
|
271
|
+
&& window.ManyoyoMarkdown
|
|
272
|
+
&& typeof window.ManyoyoMarkdown.render === 'function';
|
|
273
|
+
|
|
274
|
+
if (showMarkdownRendered) {
|
|
275
|
+
destroyEditor();
|
|
276
|
+
const markdownNode = document.createElement('div');
|
|
277
|
+
markdownNode.className = 'md-content files-markdown-preview';
|
|
278
|
+
markdownNode.innerHTML = window.ManyoyoMarkdown.render(String(payload.content || ''), {
|
|
279
|
+
imageUrlResolver: function (relativeHref) {
|
|
280
|
+
return resolveMarkdownImageUrl(payload.path, relativeHref);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
previewBodyNode.innerHTML = '';
|
|
284
|
+
previewBodyNode.appendChild(markdownNode);
|
|
285
|
+
syncMarkdownToggleButton();
|
|
286
|
+
syncSaveButton();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
235
290
|
if (window.ManyoyoCodeEditor && typeof window.ManyoyoCodeEditor.create === 'function') {
|
|
236
291
|
if (!state.editor || !state.editorHost || !previewBodyNode.contains(state.editorHost)) {
|
|
237
292
|
destroyEditor();
|
|
@@ -252,18 +307,31 @@
|
|
|
252
307
|
state.editor.setLanguage(language);
|
|
253
308
|
state.editor.setReadOnly(state.previewReadOnly);
|
|
254
309
|
}
|
|
310
|
+
syncMarkdownToggleButton();
|
|
255
311
|
syncSaveButton();
|
|
256
312
|
return;
|
|
257
313
|
}
|
|
258
314
|
|
|
259
315
|
destroyEditor();
|
|
260
316
|
previewBodyNode.innerHTML = `<pre class="files-pre">${escapeHtml(String(payload.content || ''))}</pre>`;
|
|
317
|
+
syncMarkdownToggleButton();
|
|
318
|
+
syncSaveButton();
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (payload.kind === 'image') {
|
|
323
|
+
destroyEditor();
|
|
324
|
+
const rawUrl = '/api/sessions/' + encodeURIComponent(state.sessionName) + '/fs/raw?path='
|
|
325
|
+
+ encodeURIComponent(payload.path);
|
|
326
|
+
previewBodyNode.innerHTML = `<div class="files-image-preview"><img src="${escapeHtml(rawUrl)}" alt="${escapeHtml(payload.path || '')}" /></div>`;
|
|
327
|
+
syncMarkdownToggleButton();
|
|
261
328
|
syncSaveButton();
|
|
262
329
|
return;
|
|
263
330
|
}
|
|
264
331
|
|
|
265
332
|
destroyEditor();
|
|
266
333
|
previewBodyNode.innerHTML = `<div class="files-note">当前文件暂不支持在线预览。文件类型:${escapeHtml(payload.kind || 'unknown')}</div>`;
|
|
334
|
+
syncMarkdownToggleButton();
|
|
267
335
|
syncSaveButton();
|
|
268
336
|
}
|
|
269
337
|
|
|
@@ -397,6 +465,7 @@
|
|
|
397
465
|
state.loadingFile = true;
|
|
398
466
|
state.selectedPath = pathText;
|
|
399
467
|
state.selectedEntry = selectedEntry;
|
|
468
|
+
state.markdownViewMode = 'rendered';
|
|
400
469
|
renderList();
|
|
401
470
|
renderPreviewEmpty(pathText, '正在读取文件内容...');
|
|
402
471
|
try {
|
|
@@ -584,6 +653,13 @@
|
|
|
584
653
|
});
|
|
585
654
|
}
|
|
586
655
|
|
|
656
|
+
if (toggleMarkdownViewBtn) {
|
|
657
|
+
toggleMarkdownViewBtn.addEventListener('click', function () {
|
|
658
|
+
state.markdownViewMode = state.markdownViewMode === 'source' ? 'rendered' : 'source';
|
|
659
|
+
renderPreviewPayload(state.selectedFile);
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
|
|
587
663
|
renderList();
|
|
588
664
|
|
|
589
665
|
return {
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
configured: false,
|
|
6
6
|
available: false,
|
|
7
7
|
linkGuardBound: false,
|
|
8
|
-
linkOpenHandler: null
|
|
8
|
+
linkOpenHandler: null,
|
|
9
|
+
currentImageResolver: null
|
|
9
10
|
};
|
|
10
11
|
|
|
11
12
|
function escapeHtml(value) {
|
|
@@ -247,8 +248,14 @@
|
|
|
247
248
|
'[\uD83D\uDDBC\uFE0F点击查看图片:' + (safeText || escapeHtml(safeHref)) + ']'
|
|
248
249
|
);
|
|
249
250
|
}
|
|
250
|
-
//
|
|
251
|
-
|
|
251
|
+
// 相对路径:交给调用方按当前渲染上下文解析为可访问的 URL(未提供 resolver 时原样使用)
|
|
252
|
+
const resolvedHref = typeof runtime.currentImageResolver === 'function'
|
|
253
|
+
? runtime.currentImageResolver(safeHref)
|
|
254
|
+
: safeHref;
|
|
255
|
+
if (!resolvedHref) {
|
|
256
|
+
return safeText || escapeHtml(token ? token.text : text || '');
|
|
257
|
+
}
|
|
258
|
+
let output = '<img src="' + escapeHtml(resolvedHref) + '" alt="' + escapeHtml(text || '') + '"';
|
|
252
259
|
if (rawTitle) {
|
|
253
260
|
output += ' title="' + escapeHtml(rawTitle) + '"';
|
|
254
261
|
}
|
|
@@ -272,7 +279,7 @@
|
|
|
272
279
|
return Boolean(msg && msg.mode === 'agent' && msg.role === 'assistant');
|
|
273
280
|
}
|
|
274
281
|
|
|
275
|
-
function render(content) {
|
|
282
|
+
function render(content, options) {
|
|
276
283
|
const source = String(content == null ? '' : content);
|
|
277
284
|
if (!source) {
|
|
278
285
|
return '';
|
|
@@ -284,10 +291,15 @@
|
|
|
284
291
|
if (!markedApi) {
|
|
285
292
|
return '';
|
|
286
293
|
}
|
|
294
|
+
runtime.currentImageResolver = options && typeof options.imageUrlResolver === 'function'
|
|
295
|
+
? options.imageUrlResolver
|
|
296
|
+
: null;
|
|
287
297
|
try {
|
|
288
298
|
return String(markedApi.parse(source) || '');
|
|
289
299
|
} catch (e) {
|
|
290
300
|
return '';
|
|
301
|
+
} finally {
|
|
302
|
+
runtime.currentImageResolver = null;
|
|
291
303
|
}
|
|
292
304
|
}
|
|
293
305
|
|
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
.
|
|
1
|
+
.md-content {
|
|
2
2
|
color: var(--text);
|
|
3
3
|
font-size: 13px;
|
|
4
4
|
line-height: 1.6;
|
|
5
5
|
word-break: break-word;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
.
|
|
8
|
+
.md-content > :first-child {
|
|
9
9
|
margin-top: 0;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
.
|
|
12
|
+
.md-content > :last-child {
|
|
13
13
|
margin-bottom: 0;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
.
|
|
17
|
-
.
|
|
18
|
-
.
|
|
19
|
-
.
|
|
20
|
-
.
|
|
21
|
-
.
|
|
16
|
+
.md-content p,
|
|
17
|
+
.md-content ul,
|
|
18
|
+
.md-content ol,
|
|
19
|
+
.md-content blockquote,
|
|
20
|
+
.md-content pre,
|
|
21
|
+
.md-content table {
|
|
22
22
|
margin: 0.6em 0;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
.
|
|
26
|
-
.
|
|
25
|
+
.md-content ul,
|
|
26
|
+
.md-content ol {
|
|
27
27
|
padding-left: 1.4em;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
.
|
|
30
|
+
.md-content code {
|
|
31
31
|
font-family: var(--font-mono);
|
|
32
32
|
font-size: 12px;
|
|
33
33
|
padding: 1px 5px;
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
background: rgba(194, 149, 79, 0.16);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
.
|
|
38
|
+
.md-content pre {
|
|
39
39
|
border-radius: 8px;
|
|
40
40
|
padding: 9px 10px;
|
|
41
41
|
border: 1px solid #e6d2b7;
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
overflow-x: auto;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
.
|
|
46
|
+
.md-content pre code {
|
|
47
47
|
background: transparent;
|
|
48
48
|
border-radius: 0;
|
|
49
49
|
padding: 0;
|
|
@@ -51,26 +51,26 @@
|
|
|
51
51
|
line-height: 1.5;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
.
|
|
54
|
+
.md-content blockquote {
|
|
55
55
|
margin-left: 0;
|
|
56
56
|
padding-left: 10px;
|
|
57
57
|
border-left: 3px solid #dcb788;
|
|
58
58
|
color: #66492d;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
.
|
|
61
|
+
.md-content table {
|
|
62
62
|
border-collapse: collapse;
|
|
63
63
|
width: 100%;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
.
|
|
67
|
-
.
|
|
66
|
+
.md-content th,
|
|
67
|
+
.md-content td {
|
|
68
68
|
border: 1px solid #e6d2b7;
|
|
69
69
|
padding: 4px 6px;
|
|
70
70
|
text-align: left;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
.
|
|
73
|
+
.md-content a {
|
|
74
74
|
color: #8a4f17;
|
|
75
75
|
text-decoration-line: underline;
|
|
76
76
|
text-decoration-thickness: 2px;
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
text-decoration-color: rgba(138, 79, 23, 0.8);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
.
|
|
82
|
-
.
|
|
81
|
+
.md-content a:hover,
|
|
82
|
+
.md-content a:focus-visible {
|
|
83
83
|
text-decoration-color: #8a4f17;
|
|
84
84
|
}
|
package/lib/web/server.js
CHANGED
|
@@ -45,6 +45,16 @@ const WEB_AGENT_CONTEXT_PER_MESSAGE_MAX_CHARS = 600;
|
|
|
45
45
|
const NATIVE_SESSION_RESUME_PROGRAMS = new Set(['claude', 'codex', 'opencode']);
|
|
46
46
|
const WEB_FILE_PREVIEW_MAX_BYTES = 512 * 1024;
|
|
47
47
|
const WEB_FILE_EDIT_MAX_BYTES = 2 * 1024 * 1024;
|
|
48
|
+
const WEB_FILE_RAW_MAX_CONCURRENT_PER_CONTAINER = 4;
|
|
49
|
+
const IMAGE_EXTENSION_CONTENT_TYPES = {
|
|
50
|
+
'.png': 'image/png',
|
|
51
|
+
'.jpg': 'image/jpeg',
|
|
52
|
+
'.jpeg': 'image/jpeg',
|
|
53
|
+
'.gif': 'image/gif',
|
|
54
|
+
'.webp': 'image/webp',
|
|
55
|
+
'.bmp': 'image/bmp',
|
|
56
|
+
'.ico': 'image/x-icon'
|
|
57
|
+
};
|
|
48
58
|
const WEB_AUTH_COOKIE_NAME = 'manyoyo_web_auth';
|
|
49
59
|
const WEB_AUTH_TTL_SECONDS = 12 * 60 * 60;
|
|
50
60
|
const WEB_SESSION_KEY_SEPARATOR = '~';
|
|
@@ -1473,6 +1483,7 @@ function prepareCodexTraceEvent(payload) {
|
|
|
1473
1483
|
const mcpServer = pickFirstString(item.server);
|
|
1474
1484
|
const mcpTool = pickFirstString(item.tool);
|
|
1475
1485
|
const itemStatus = pickFirstString(item.status);
|
|
1486
|
+
const toolId = pickFirstString(item.id);
|
|
1476
1487
|
|
|
1477
1488
|
function shortenText(value, maxChars = 140) {
|
|
1478
1489
|
const raw = clipText(stripAnsi(String(value || '')).replace(/\s+/g, ' ').trim(), maxChars);
|
|
@@ -1548,14 +1559,16 @@ function prepareCodexTraceEvent(payload) {
|
|
|
1548
1559
|
return createTraceEvent('tool', `[工具开始] ${toolName || 'tool_call'}`, {
|
|
1549
1560
|
phase: 'started',
|
|
1550
1561
|
status: pickDisplayStatus('in_progress'),
|
|
1551
|
-
toolName: toolName || 'tool_call'
|
|
1562
|
+
toolName: toolName || 'tool_call',
|
|
1563
|
+
toolId
|
|
1552
1564
|
});
|
|
1553
1565
|
}
|
|
1554
1566
|
if (itemType === 'command_execution') {
|
|
1555
1567
|
return createTraceEvent('command', `[命令开始] ${commandText || 'command_execution'}`, {
|
|
1556
1568
|
phase: 'started',
|
|
1557
1569
|
status: pickDisplayStatus('in_progress'),
|
|
1558
|
-
command: commandText || 'command_execution'
|
|
1570
|
+
command: commandText || 'command_execution',
|
|
1571
|
+
toolId
|
|
1559
1572
|
});
|
|
1560
1573
|
}
|
|
1561
1574
|
if (itemType === 'mcp_tool_call') {
|
|
@@ -1573,7 +1586,8 @@ function prepareCodexTraceEvent(payload) {
|
|
|
1573
1586
|
arguments: item.arguments && typeof item.arguments === 'object' && !Array.isArray(item.arguments)
|
|
1574
1587
|
? item.arguments
|
|
1575
1588
|
: null,
|
|
1576
|
-
argumentSummary: summary
|
|
1589
|
+
argumentSummary: summary,
|
|
1590
|
+
toolId
|
|
1577
1591
|
}
|
|
1578
1592
|
);
|
|
1579
1593
|
}
|
|
@@ -1602,7 +1616,8 @@ function prepareCodexTraceEvent(payload) {
|
|
|
1602
1616
|
return createTraceEvent('tool', `[工具完成] ${toolName || 'tool_call'}`, {
|
|
1603
1617
|
phase: 'completed',
|
|
1604
1618
|
status: pickDisplayStatus('completed'),
|
|
1605
|
-
toolName: toolName || 'tool_call'
|
|
1619
|
+
toolName: toolName || 'tool_call',
|
|
1620
|
+
toolId
|
|
1606
1621
|
});
|
|
1607
1622
|
}
|
|
1608
1623
|
if (itemType === 'command_execution') {
|
|
@@ -1612,7 +1627,8 @@ function prepareCodexTraceEvent(payload) {
|
|
|
1612
1627
|
status: pickDisplayStatus(suffix),
|
|
1613
1628
|
command: commandText || 'command_execution',
|
|
1614
1629
|
exitCode: typeof item.exit_code === 'number' ? item.exit_code : null,
|
|
1615
|
-
result: item.aggregated_output !== undefined ? item.aggregated_output : null
|
|
1630
|
+
result: item.aggregated_output !== undefined ? item.aggregated_output : null,
|
|
1631
|
+
toolId
|
|
1616
1632
|
});
|
|
1617
1633
|
}
|
|
1618
1634
|
if (itemType === 'mcp_tool_call') {
|
|
@@ -1632,7 +1648,8 @@ function prepareCodexTraceEvent(payload) {
|
|
|
1632
1648
|
: null,
|
|
1633
1649
|
argumentSummary: summary,
|
|
1634
1650
|
result: item.result !== undefined ? item.result : null,
|
|
1635
|
-
error: item.error !== undefined ? item.error : null
|
|
1651
|
+
error: item.error !== undefined ? item.error : null,
|
|
1652
|
+
toolId
|
|
1636
1653
|
}
|
|
1637
1654
|
);
|
|
1638
1655
|
}
|
|
@@ -2930,6 +2947,34 @@ ${scriptSource}
|
|
|
2930
2947
|
__MANYOYO_NODE__`;
|
|
2931
2948
|
}
|
|
2932
2949
|
|
|
2950
|
+
function spawnContainerRawFileStream(ctx, containerName, filePath) {
|
|
2951
|
+
return spawn(
|
|
2952
|
+
ctx.dockerCmd,
|
|
2953
|
+
['exec', containerName, 'cat', '--', filePath],
|
|
2954
|
+
{ stdio: ['ignore', 'pipe', 'pipe'] }
|
|
2955
|
+
);
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2958
|
+
const webRawFileStreamCounts = new Map();
|
|
2959
|
+
|
|
2960
|
+
function acquireWebRawFileStreamSlot(containerName) {
|
|
2961
|
+
const current = webRawFileStreamCounts.get(containerName) || 0;
|
|
2962
|
+
if (current >= WEB_FILE_RAW_MAX_CONCURRENT_PER_CONTAINER) {
|
|
2963
|
+
return false;
|
|
2964
|
+
}
|
|
2965
|
+
webRawFileStreamCounts.set(containerName, current + 1);
|
|
2966
|
+
return true;
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2969
|
+
function releaseWebRawFileStreamSlot(containerName) {
|
|
2970
|
+
const current = webRawFileStreamCounts.get(containerName) || 0;
|
|
2971
|
+
if (current <= 1) {
|
|
2972
|
+
webRawFileStreamCounts.delete(containerName);
|
|
2973
|
+
return;
|
|
2974
|
+
}
|
|
2975
|
+
webRawFileStreamCounts.set(containerName, current - 1);
|
|
2976
|
+
}
|
|
2977
|
+
|
|
2933
2978
|
function inferFileLanguage(filePath) {
|
|
2934
2979
|
const ext = path.extname(String(filePath || '')).toLowerCase();
|
|
2935
2980
|
return FILE_LANGUAGE_MAP[ext] || 'text';
|
|
@@ -3018,9 +3063,11 @@ function buildContainerFileReadCommand(requestedPath, options = {}) {
|
|
|
3018
3063
|
return buildWebContainerNodeCommand(`
|
|
3019
3064
|
// __MANYOYO_FS_READ__
|
|
3020
3065
|
const fs = require('fs');
|
|
3066
|
+
const path = require('path');
|
|
3021
3067
|
|
|
3022
3068
|
const requestedPath = ${JSON.stringify(String(requestedPath || ''))};
|
|
3023
3069
|
const maxBytes = ${String(maxBytes)};
|
|
3070
|
+
const imageExtensions = new Set(${JSON.stringify(Object.keys(IMAGE_EXTENSION_CONTENT_TYPES))});
|
|
3024
3071
|
|
|
3025
3072
|
function looksBinary(buffer) {
|
|
3026
3073
|
const length = Math.min(buffer.length, 4096);
|
|
@@ -3044,32 +3091,66 @@ try {
|
|
|
3044
3091
|
throw new Error('目标不是文件: ' + realPath);
|
|
3045
3092
|
}
|
|
3046
3093
|
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
const buffer = Buffer.alloc(readBytes);
|
|
3050
|
-
const fd = fs.openSync(realPath, 'r');
|
|
3051
|
-
try {
|
|
3052
|
-
fs.readSync(fd, buffer, 0, readBytes, 0);
|
|
3053
|
-
} finally {
|
|
3054
|
-
fs.closeSync(fd);
|
|
3055
|
-
}
|
|
3056
|
-
|
|
3057
|
-
if (looksBinary(buffer)) {
|
|
3094
|
+
// 图片走 fs/raw 流式传输,这里只确认元信息,不把整份二进制读进内存/JSON
|
|
3095
|
+
if (imageExtensions.has(path.extname(realPath).toLowerCase())) {
|
|
3058
3096
|
process.stdout.write(JSON.stringify({
|
|
3059
3097
|
path: realPath,
|
|
3060
|
-
kind: '
|
|
3061
|
-
size
|
|
3062
|
-
truncated: maxBytes > 0 && size > maxBytes
|
|
3098
|
+
kind: 'image',
|
|
3099
|
+
size: stat.size
|
|
3063
3100
|
}));
|
|
3064
3101
|
} else {
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
}
|
|
3102
|
+
const size = stat.size;
|
|
3103
|
+
const readBytes = maxBytes > 0 ? Math.min(size, maxBytes) : size;
|
|
3104
|
+
const buffer = Buffer.alloc(readBytes);
|
|
3105
|
+
const fd = fs.openSync(realPath, 'r');
|
|
3106
|
+
try {
|
|
3107
|
+
fs.readSync(fd, buffer, 0, readBytes, 0);
|
|
3108
|
+
} finally {
|
|
3109
|
+
fs.closeSync(fd);
|
|
3110
|
+
}
|
|
3111
|
+
|
|
3112
|
+
if (looksBinary(buffer)) {
|
|
3113
|
+
process.stdout.write(JSON.stringify({
|
|
3114
|
+
path: realPath,
|
|
3115
|
+
kind: 'binary',
|
|
3116
|
+
size,
|
|
3117
|
+
truncated: maxBytes > 0 && size > maxBytes
|
|
3118
|
+
}));
|
|
3119
|
+
} else {
|
|
3120
|
+
process.stdout.write(JSON.stringify({
|
|
3121
|
+
path: realPath,
|
|
3122
|
+
kind: 'text',
|
|
3123
|
+
size,
|
|
3124
|
+
truncated: maxBytes > 0 && size > maxBytes,
|
|
3125
|
+
content: buffer.toString('utf8')
|
|
3126
|
+
}));
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
} catch (e) {
|
|
3130
|
+
process.stdout.write(JSON.stringify({
|
|
3131
|
+
error: e && e.message ? e.message : '读取文件失败'
|
|
3132
|
+
}));
|
|
3133
|
+
}
|
|
3134
|
+
`);
|
|
3135
|
+
}
|
|
3136
|
+
|
|
3137
|
+
function buildContainerFileStatCommand(requestedPath) {
|
|
3138
|
+
return buildWebContainerNodeCommand(`
|
|
3139
|
+
// __MANYOYO_FS_STAT__
|
|
3140
|
+
const fs = require('fs');
|
|
3141
|
+
|
|
3142
|
+
const requestedPath = ${JSON.stringify(String(requestedPath || ''))};
|
|
3143
|
+
|
|
3144
|
+
try {
|
|
3145
|
+
const realPath = fs.realpathSync(requestedPath);
|
|
3146
|
+
const stat = fs.statSync(realPath);
|
|
3147
|
+
if (!stat.isFile()) {
|
|
3148
|
+
throw new Error('目标不是文件: ' + realPath);
|
|
3072
3149
|
}
|
|
3150
|
+
process.stdout.write(JSON.stringify({
|
|
3151
|
+
path: realPath,
|
|
3152
|
+
size: stat.size
|
|
3153
|
+
}));
|
|
3073
3154
|
} catch (e) {
|
|
3074
3155
|
process.stdout.write(JSON.stringify({
|
|
3075
3156
|
error: e && e.message ? e.message : '读取文件失败'
|
|
@@ -4316,6 +4397,73 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
4316
4397
|
sendJson(res, 200, payload);
|
|
4317
4398
|
}
|
|
4318
4399
|
},
|
|
4400
|
+
{
|
|
4401
|
+
method: 'GET',
|
|
4402
|
+
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/fs\/raw$/),
|
|
4403
|
+
handler: async match => {
|
|
4404
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
4405
|
+
if (!sessionRef) {
|
|
4406
|
+
return;
|
|
4407
|
+
}
|
|
4408
|
+
const requestUrl = new URL(req.url || '/api/sessions/x/fs/raw', 'http://localhost');
|
|
4409
|
+
const targetPath = String(requestUrl.searchParams.get('path') || '').trim();
|
|
4410
|
+
if (!targetPath) {
|
|
4411
|
+
sendJson(res, 400, { error: 'path 不能为空' });
|
|
4412
|
+
return;
|
|
4413
|
+
}
|
|
4414
|
+
const ext = path.extname(targetPath).toLowerCase();
|
|
4415
|
+
const contentType = IMAGE_EXTENSION_CONTENT_TYPES[ext];
|
|
4416
|
+
if (!contentType) {
|
|
4417
|
+
sendJson(res, 400, { error: '不支持预览该文件类型' });
|
|
4418
|
+
return;
|
|
4419
|
+
}
|
|
4420
|
+
|
|
4421
|
+
await ensureWebContainer(ctx, state, sessionRef.containerName, sessionRef);
|
|
4422
|
+
const statPayload = await execJsonCommandInWebContainer(
|
|
4423
|
+
ctx,
|
|
4424
|
+
sessionRef.containerName,
|
|
4425
|
+
buildContainerFileStatCommand(targetPath)
|
|
4426
|
+
);
|
|
4427
|
+
if (statPayload && statPayload.error) {
|
|
4428
|
+
sendJson(res, 404, { error: statPayload.error });
|
|
4429
|
+
return;
|
|
4430
|
+
}
|
|
4431
|
+
|
|
4432
|
+
if (!acquireWebRawFileStreamSlot(sessionRef.containerName)) {
|
|
4433
|
+
sendJson(res, 429, { error: '预览并发已达上限,请稍后重试' });
|
|
4434
|
+
return;
|
|
4435
|
+
}
|
|
4436
|
+
|
|
4437
|
+
let released = false;
|
|
4438
|
+
const releaseOnce = () => {
|
|
4439
|
+
if (released) {
|
|
4440
|
+
return;
|
|
4441
|
+
}
|
|
4442
|
+
released = true;
|
|
4443
|
+
releaseWebRawFileStreamSlot(sessionRef.containerName);
|
|
4444
|
+
};
|
|
4445
|
+
|
|
4446
|
+
const child = spawnContainerRawFileStream(ctx, sessionRef.containerName, statPayload.path);
|
|
4447
|
+
// stderr 单独消费,绝不混入响应体,避免管道缓冲区打满导致 cat 进程卡死
|
|
4448
|
+
child.stderr.on('data', () => {});
|
|
4449
|
+
child.on('error', () => releaseOnce());
|
|
4450
|
+
child.on('close', () => releaseOnce());
|
|
4451
|
+
res.on('close', () => {
|
|
4452
|
+
releaseOnce();
|
|
4453
|
+
if (!child.killed) {
|
|
4454
|
+
child.kill();
|
|
4455
|
+
}
|
|
4456
|
+
});
|
|
4457
|
+
|
|
4458
|
+
res.writeHead(200, {
|
|
4459
|
+
'Content-Type': contentType,
|
|
4460
|
+
'Content-Length': String(statPayload.size),
|
|
4461
|
+
'X-Content-Type-Options': 'nosniff',
|
|
4462
|
+
'Cache-Control': 'no-store'
|
|
4463
|
+
});
|
|
4464
|
+
child.stdout.pipe(res);
|
|
4465
|
+
}
|
|
4466
|
+
},
|
|
4319
4467
|
{
|
|
4320
4468
|
method: 'PUT',
|
|
4321
4469
|
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/fs\/write$/),
|