@xcanwin/manyoyo 6.2.14 → 6.2.17
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 +57 -3
- package/lib/web/frontend/file-browser.js +81 -8
- package/lib/web/server.js +63 -0
- package/package.json +1 -1
package/lib/web/frontend/app.css
CHANGED
|
@@ -1082,7 +1082,7 @@ body.workspace-switcher-open .workspace-switcher-panel {
|
|
|
1082
1082
|
display: flex;
|
|
1083
1083
|
align-items: center;
|
|
1084
1084
|
gap: 8px;
|
|
1085
|
-
flex-wrap:
|
|
1085
|
+
flex-wrap: nowrap;
|
|
1086
1086
|
padding: 12px 14px;
|
|
1087
1087
|
border-bottom: 1px solid var(--line);
|
|
1088
1088
|
background: rgba(255, 251, 244, 0.96);
|
|
@@ -1218,7 +1218,7 @@ body.workspace-switcher-open .workspace-switcher-panel {
|
|
|
1218
1218
|
align-items: flex-start;
|
|
1219
1219
|
justify-content: space-between;
|
|
1220
1220
|
gap: 12px;
|
|
1221
|
-
flex-wrap:
|
|
1221
|
+
flex-wrap: nowrap;
|
|
1222
1222
|
padding: 10px 12px;
|
|
1223
1223
|
border-bottom: 1px solid var(--line);
|
|
1224
1224
|
background: rgba(255, 251, 244, 0.96);
|
|
@@ -1240,6 +1240,11 @@ body.workspace-switcher-open .workspace-switcher-panel {
|
|
|
1240
1240
|
margin-left: auto;
|
|
1241
1241
|
}
|
|
1242
1242
|
|
|
1243
|
+
.files-back-btn {
|
|
1244
|
+
display: none;
|
|
1245
|
+
flex-shrink: 0;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1243
1248
|
.files-preview-title {
|
|
1244
1249
|
font-family: var(--font-display);
|
|
1245
1250
|
font-size: 14px;
|
|
@@ -2162,7 +2167,7 @@ body.composer-options-open .composer-options-panel {
|
|
|
2162
2167
|
|
|
2163
2168
|
.files-layout {
|
|
2164
2169
|
grid-template-columns: minmax(0, 1fr);
|
|
2165
|
-
grid-template-rows: minmax(
|
|
2170
|
+
grid-template-rows: minmax(0, 1fr);
|
|
2166
2171
|
}
|
|
2167
2172
|
|
|
2168
2173
|
.files-sidebar {
|
|
@@ -2170,6 +2175,55 @@ body.composer-options-open .composer-options-panel {
|
|
|
2170
2175
|
border-bottom: 1px solid var(--line);
|
|
2171
2176
|
}
|
|
2172
2177
|
|
|
2178
|
+
.files-toolbar {
|
|
2179
|
+
flex-wrap: wrap;
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
.files-toolbar-path-group {
|
|
2183
|
+
flex-basis: 100%;
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
.files-toolbar-meta {
|
|
2187
|
+
flex-basis: 100%;
|
|
2188
|
+
flex-wrap: wrap;
|
|
2189
|
+
margin-left: 0;
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
.files-browser[data-mobile-pane="list"] .files-preview {
|
|
2193
|
+
display: none;
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
.files-browser[data-mobile-pane="preview"] .files-sidebar {
|
|
2197
|
+
display: none;
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
.files-browser[data-mobile-pane="preview"] .files-toolbar {
|
|
2201
|
+
display: none;
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
.files-browser[data-mobile-pane="preview"] .files-back-btn {
|
|
2205
|
+
display: inline-flex;
|
|
2206
|
+
order: 1;
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
.files-preview-head {
|
|
2210
|
+
flex-wrap: wrap;
|
|
2211
|
+
align-items: center;
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
.files-preview-actions {
|
|
2215
|
+
order: 2;
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
.files-preview-head-main {
|
|
2219
|
+
order: 3;
|
|
2220
|
+
flex-basis: 100%;
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
.files-preview-meta {
|
|
2224
|
+
display: none;
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2173
2227
|
.composer-inner {
|
|
2174
2228
|
align-items: stretch;
|
|
2175
2229
|
}
|
|
@@ -99,6 +99,7 @@
|
|
|
99
99
|
<div class="files-toolbar-meta">
|
|
100
100
|
<div class="files-toolbar-status" data-role="status">未加载</div>
|
|
101
101
|
<button type="button" class="secondary" data-action="mkdir">新建目录</button>
|
|
102
|
+
<button type="button" class="secondary" data-action="new-file">新建文件</button>
|
|
102
103
|
</div>
|
|
103
104
|
</header>
|
|
104
105
|
<div class="files-layout">
|
|
@@ -107,6 +108,7 @@
|
|
|
107
108
|
</aside>
|
|
108
109
|
<section class="files-preview">
|
|
109
110
|
<header class="files-preview-head">
|
|
111
|
+
<button type="button" class="secondary files-back-btn" data-action="back-to-list">← 返回列表</button>
|
|
110
112
|
<div class="files-preview-head-main">
|
|
111
113
|
<div class="files-preview-title" data-role="preview-title">未选择文件</div>
|
|
112
114
|
<div class="files-preview-meta" data-role="preview-meta">请选择左侧文件或目录</div>
|
|
@@ -128,10 +130,13 @@
|
|
|
128
130
|
const previewTitleNode = root.querySelector('[data-role="preview-title"]');
|
|
129
131
|
const previewMetaNode = root.querySelector('[data-role="preview-meta"]');
|
|
130
132
|
const previewBodyNode = root.querySelector('[data-role="preview-body"]');
|
|
133
|
+
const filesBrowserNode = root.querySelector('.files-browser');
|
|
131
134
|
const visitBtn = root.querySelector('[data-action="visit"]');
|
|
132
135
|
const mkdirBtn = root.querySelector('[data-action="mkdir"]');
|
|
136
|
+
const newFileBtn = root.querySelector('[data-action="new-file"]');
|
|
133
137
|
const saveBtn = root.querySelector('[data-action="save"]');
|
|
134
138
|
const toggleMarkdownViewBtn = root.querySelector('[data-action="toggle-markdown-view"]');
|
|
139
|
+
const backToListBtn = root.querySelector('[data-action="back-to-list"]');
|
|
135
140
|
|
|
136
141
|
const state = {
|
|
137
142
|
visible: false,
|
|
@@ -155,9 +160,17 @@
|
|
|
155
160
|
editorHost: null,
|
|
156
161
|
previewReadOnly: true,
|
|
157
162
|
previewDirty: false,
|
|
158
|
-
markdownViewMode: 'rendered'
|
|
163
|
+
markdownViewMode: 'rendered',
|
|
164
|
+
mobileActivePane: 'list'
|
|
159
165
|
};
|
|
160
166
|
|
|
167
|
+
function setMobilePane(pane) {
|
|
168
|
+
state.mobileActivePane = pane === 'preview' ? 'preview' : 'list';
|
|
169
|
+
if (filesBrowserNode) {
|
|
170
|
+
filesBrowserNode.setAttribute('data-mobile-pane', state.mobileActivePane);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
161
174
|
function setStatus(text) {
|
|
162
175
|
if (statusNode) {
|
|
163
176
|
statusNode.textContent = String(text || '').trim() || '就绪';
|
|
@@ -249,6 +262,17 @@
|
|
|
249
262
|
return host;
|
|
250
263
|
}
|
|
251
264
|
|
|
265
|
+
function updatePreviewMeta() {
|
|
266
|
+
const payload = state.selectedFile;
|
|
267
|
+
if (!previewMetaNode || !payload) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const modeLabel = payload.kind === 'text'
|
|
271
|
+
? (payload.editable === true ? '可编辑' : '只读预览')
|
|
272
|
+
: '只读预览';
|
|
273
|
+
previewMetaNode.textContent = `${payload.kind === 'text' ? '文本文件' : '文件'} · ${formatBytes(payload.size)} · ${modeLabel}${payload.truncated ? ' · 已截断预览' : ''}`;
|
|
274
|
+
}
|
|
275
|
+
|
|
252
276
|
function renderPreviewPayload(payload) {
|
|
253
277
|
state.selectedFile = payload || null;
|
|
254
278
|
state.previewReadOnly = !(payload && payload.editable === true);
|
|
@@ -260,12 +284,7 @@
|
|
|
260
284
|
if (previewTitleNode) {
|
|
261
285
|
previewTitleNode.textContent = payload.path || '未命名文件';
|
|
262
286
|
}
|
|
263
|
-
|
|
264
|
-
const modeLabel = payload.kind === 'text'
|
|
265
|
-
? (payload.editable === true ? '可编辑' : '只读预览')
|
|
266
|
-
: '只读预览';
|
|
267
|
-
previewMetaNode.textContent = `${payload.kind === 'text' ? '文本文件' : '文件'} · ${formatBytes(payload.size)} · ${modeLabel}${payload.truncated ? ' · 已截断预览' : ''}`;
|
|
268
|
-
}
|
|
287
|
+
updatePreviewMeta();
|
|
269
288
|
if (!previewBodyNode) {
|
|
270
289
|
syncMarkdownToggleButton();
|
|
271
290
|
syncSaveButton();
|
|
@@ -304,8 +323,12 @@
|
|
|
304
323
|
doc: String(payload.content || ''),
|
|
305
324
|
language,
|
|
306
325
|
readOnly: state.previewReadOnly,
|
|
307
|
-
onChange: function () {
|
|
326
|
+
onChange: function (nextValue) {
|
|
308
327
|
state.previewDirty = true;
|
|
328
|
+
if (state.selectedFile) {
|
|
329
|
+
state.selectedFile.size = new TextEncoder().encode(nextValue).length;
|
|
330
|
+
}
|
|
331
|
+
updatePreviewMeta();
|
|
309
332
|
syncSaveButton();
|
|
310
333
|
}
|
|
311
334
|
});
|
|
@@ -353,18 +376,23 @@
|
|
|
353
376
|
if (mkdirBtn) {
|
|
354
377
|
mkdirBtn.disabled = state.loadingList || state.loadingFile || !state.sessionName || state.historyOnly === true;
|
|
355
378
|
}
|
|
379
|
+
if (newFileBtn) {
|
|
380
|
+
newFileBtn.disabled = state.loadingList || state.loadingFile || !state.sessionName || state.historyOnly === true;
|
|
381
|
+
}
|
|
356
382
|
if (!listNode) {
|
|
357
383
|
return;
|
|
358
384
|
}
|
|
359
385
|
listNode.innerHTML = '';
|
|
360
386
|
|
|
361
387
|
if (!state.sessionName) {
|
|
388
|
+
setMobilePane('list');
|
|
362
389
|
listNode.innerHTML = '<div class="files-empty">请选择左侧会话后再浏览容器文件。</div>';
|
|
363
390
|
renderPreviewEmpty('未选择会话', '请选择左侧会话后再浏览容器文件。');
|
|
364
391
|
setStatus('未选择会话');
|
|
365
392
|
return;
|
|
366
393
|
}
|
|
367
394
|
if (state.historyOnly) {
|
|
395
|
+
setMobilePane('list');
|
|
368
396
|
listNode.innerHTML = '<div class="files-empty">当前会话只有历史记录,没有可访问的运行中容器。</div>';
|
|
369
397
|
renderPreviewEmpty('容器不可用', '当前会话只有历史记录,没有可访问的运行中容器。');
|
|
370
398
|
setStatus('容器不可用');
|
|
@@ -429,6 +457,7 @@
|
|
|
429
457
|
state.pathDraft = pathText;
|
|
430
458
|
state.selectedPath = '';
|
|
431
459
|
state.selectedEntry = null;
|
|
460
|
+
setMobilePane('list');
|
|
432
461
|
renderList();
|
|
433
462
|
try {
|
|
434
463
|
const payload = await api('/api/sessions/' + encodeURIComponent(state.sessionName) + '/fs/list?path=' + encodeURIComponent(pathText));
|
|
@@ -474,6 +503,7 @@
|
|
|
474
503
|
state.selectedPath = pathText;
|
|
475
504
|
state.selectedEntry = selectedEntry;
|
|
476
505
|
state.markdownViewMode = 'rendered';
|
|
506
|
+
setMobilePane('preview');
|
|
477
507
|
renderList();
|
|
478
508
|
renderPreviewEmpty(pathText, '正在读取文件内容...');
|
|
479
509
|
try {
|
|
@@ -581,6 +611,30 @@
|
|
|
581
611
|
}
|
|
582
612
|
}
|
|
583
613
|
|
|
614
|
+
async function createFile() {
|
|
615
|
+
if (!state.sessionName || state.historyOnly === true || state.loadingList || state.loadingFile) {
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
const input = await promptFn('请输入新文件名称');
|
|
619
|
+
const name = String(input || '').trim();
|
|
620
|
+
if (!name) {
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
const targetPath = joinDirectoryPath(state.currentPath || state.containerPath || '/', name);
|
|
624
|
+
setStatus('创建文件中');
|
|
625
|
+
try {
|
|
626
|
+
await api('/api/sessions/' + encodeURIComponent(state.sessionName) + '/fs/create', {
|
|
627
|
+
method: 'POST',
|
|
628
|
+
body: JSON.stringify({ path: targetPath })
|
|
629
|
+
});
|
|
630
|
+
await loadDirectory(state.currentPath || state.containerPath || '/');
|
|
631
|
+
setStatus('已创建文件');
|
|
632
|
+
} catch (e) {
|
|
633
|
+
setStatus('创建文件失败');
|
|
634
|
+
onError(e && e.message ? e.message : '创建文件失败');
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
584
638
|
function sync(context) {
|
|
585
639
|
const session = context && context.session;
|
|
586
640
|
const detail = context && context.detail;
|
|
@@ -609,6 +663,7 @@
|
|
|
609
663
|
state.entries = [];
|
|
610
664
|
state.selectedPath = '';
|
|
611
665
|
state.selectedEntry = null;
|
|
666
|
+
setMobilePane('list');
|
|
612
667
|
renderPreviewEmpty('未选择文件', '请选择左侧文件进行预览。');
|
|
613
668
|
} else if (containerPathChanged) {
|
|
614
669
|
state.containerPath = nextContainerPath;
|
|
@@ -653,6 +708,14 @@
|
|
|
653
708
|
});
|
|
654
709
|
}
|
|
655
710
|
|
|
711
|
+
if (newFileBtn) {
|
|
712
|
+
newFileBtn.addEventListener('click', function () {
|
|
713
|
+
createFile().catch(function (e) {
|
|
714
|
+
onError(e && e.message ? e.message : '创建文件失败');
|
|
715
|
+
});
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
|
|
656
719
|
if (saveBtn) {
|
|
657
720
|
saveBtn.addEventListener('click', function () {
|
|
658
721
|
saveCurrentFile().catch(function (e) {
|
|
@@ -663,11 +726,21 @@
|
|
|
663
726
|
|
|
664
727
|
if (toggleMarkdownViewBtn) {
|
|
665
728
|
toggleMarkdownViewBtn.addEventListener('click', function () {
|
|
729
|
+
if (state.editor && typeof state.editor.getValue === 'function' && state.selectedFile) {
|
|
730
|
+
state.selectedFile.content = state.editor.getValue();
|
|
731
|
+
}
|
|
666
732
|
state.markdownViewMode = state.markdownViewMode === 'source' ? 'rendered' : 'source';
|
|
667
733
|
renderPreviewPayload(state.selectedFile);
|
|
668
734
|
});
|
|
669
735
|
}
|
|
670
736
|
|
|
737
|
+
if (backToListBtn) {
|
|
738
|
+
backToListBtn.addEventListener('click', function () {
|
|
739
|
+
setMobilePane('list');
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
setMobilePane('list');
|
|
671
744
|
renderList();
|
|
672
745
|
|
|
673
746
|
return {
|
package/lib/web/server.js
CHANGED
|
@@ -3520,6 +3520,41 @@ try {
|
|
|
3520
3520
|
`);
|
|
3521
3521
|
}
|
|
3522
3522
|
|
|
3523
|
+
function buildContainerFileCreateCommand(requestedPath) {
|
|
3524
|
+
return buildWebContainerNodeCommand(`
|
|
3525
|
+
// __MANYOYO_FS_CREATE__
|
|
3526
|
+
const fs = require('fs');
|
|
3527
|
+
const path = require('path');
|
|
3528
|
+
|
|
3529
|
+
const requestedPath = ${JSON.stringify(String(requestedPath || ''))};
|
|
3530
|
+
|
|
3531
|
+
try {
|
|
3532
|
+
const resolvedPath = path.resolve(requestedPath);
|
|
3533
|
+
const parentPath = path.dirname(resolvedPath);
|
|
3534
|
+
const realParentPath = fs.realpathSync(parentPath);
|
|
3535
|
+
const targetPath = path.join(realParentPath, path.basename(resolvedPath));
|
|
3536
|
+
if (fs.existsSync(targetPath)) {
|
|
3537
|
+
throw new Error('文件已存在: ' + targetPath);
|
|
3538
|
+
}
|
|
3539
|
+
|
|
3540
|
+
fs.writeFileSync(targetPath, '', 'utf8');
|
|
3541
|
+
const stat = fs.statSync(targetPath);
|
|
3542
|
+
process.stdout.write(JSON.stringify({
|
|
3543
|
+
path: targetPath,
|
|
3544
|
+
name: path.basename(targetPath),
|
|
3545
|
+
kind: 'file',
|
|
3546
|
+
size: stat.size,
|
|
3547
|
+
mtimeMs: stat.mtimeMs,
|
|
3548
|
+
created: true
|
|
3549
|
+
}));
|
|
3550
|
+
} catch (e) {
|
|
3551
|
+
process.stdout.write(JSON.stringify({
|
|
3552
|
+
error: e && e.message ? e.message : '创建文件失败'
|
|
3553
|
+
}));
|
|
3554
|
+
}
|
|
3555
|
+
`);
|
|
3556
|
+
}
|
|
3557
|
+
|
|
3523
3558
|
async function execAgentInWebContainerStream(ctx, state, sessionRefOrContainerName, command, options = {}) {
|
|
3524
3559
|
const opts = options && typeof options === 'object' ? options : {};
|
|
3525
3560
|
const sessionRef = typeof sessionRefOrContainerName === 'string'
|
|
@@ -4800,6 +4835,34 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
4800
4835
|
sendJson(res, 200, result);
|
|
4801
4836
|
}
|
|
4802
4837
|
},
|
|
4838
|
+
{
|
|
4839
|
+
method: 'POST',
|
|
4840
|
+
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/fs\/create$/),
|
|
4841
|
+
handler: async match => {
|
|
4842
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
4843
|
+
if (!sessionRef) {
|
|
4844
|
+
return;
|
|
4845
|
+
}
|
|
4846
|
+
const payload = await readJsonBody(req);
|
|
4847
|
+
const targetPath = String(payload && payload.path ? payload.path : '').trim();
|
|
4848
|
+
if (!targetPath) {
|
|
4849
|
+
sendJson(res, 400, { error: 'path 不能为空' });
|
|
4850
|
+
return;
|
|
4851
|
+
}
|
|
4852
|
+
|
|
4853
|
+
await ensureWebContainer(ctx, state, sessionRef.containerName, sessionRef);
|
|
4854
|
+
const result = await execJsonCommandInWebContainer(
|
|
4855
|
+
ctx,
|
|
4856
|
+
sessionRef.containerName,
|
|
4857
|
+
buildContainerFileCreateCommand(targetPath)
|
|
4858
|
+
);
|
|
4859
|
+
if (result && result.error) {
|
|
4860
|
+
sendJson(res, 400, { error: result.error });
|
|
4861
|
+
return;
|
|
4862
|
+
}
|
|
4863
|
+
sendJson(res, 200, result);
|
|
4864
|
+
}
|
|
4865
|
+
},
|
|
4803
4866
|
{
|
|
4804
4867
|
method: 'GET',
|
|
4805
4868
|
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/detail$/),
|