@xcanwin/manyoyo 6.2.15 → 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 +15 -1
- package/lib/web/frontend/file-browser.js +57 -7
- 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);
|
|
@@ -2175,6 +2175,20 @@ body.composer-options-open .composer-options-panel {
|
|
|
2175
2175
|
border-bottom: 1px solid var(--line);
|
|
2176
2176
|
}
|
|
2177
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
|
+
|
|
2178
2192
|
.files-browser[data-mobile-pane="list"] .files-preview {
|
|
2179
2193
|
display: none;
|
|
2180
2194
|
}
|
|
@@ -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">
|
|
@@ -132,6 +133,7 @@
|
|
|
132
133
|
const filesBrowserNode = root.querySelector('.files-browser');
|
|
133
134
|
const visitBtn = root.querySelector('[data-action="visit"]');
|
|
134
135
|
const mkdirBtn = root.querySelector('[data-action="mkdir"]');
|
|
136
|
+
const newFileBtn = root.querySelector('[data-action="new-file"]');
|
|
135
137
|
const saveBtn = root.querySelector('[data-action="save"]');
|
|
136
138
|
const toggleMarkdownViewBtn = root.querySelector('[data-action="toggle-markdown-view"]');
|
|
137
139
|
const backToListBtn = root.querySelector('[data-action="back-to-list"]');
|
|
@@ -260,6 +262,17 @@
|
|
|
260
262
|
return host;
|
|
261
263
|
}
|
|
262
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
|
+
|
|
263
276
|
function renderPreviewPayload(payload) {
|
|
264
277
|
state.selectedFile = payload || null;
|
|
265
278
|
state.previewReadOnly = !(payload && payload.editable === true);
|
|
@@ -271,12 +284,7 @@
|
|
|
271
284
|
if (previewTitleNode) {
|
|
272
285
|
previewTitleNode.textContent = payload.path || '未命名文件';
|
|
273
286
|
}
|
|
274
|
-
|
|
275
|
-
const modeLabel = payload.kind === 'text'
|
|
276
|
-
? (payload.editable === true ? '可编辑' : '只读预览')
|
|
277
|
-
: '只读预览';
|
|
278
|
-
previewMetaNode.textContent = `${payload.kind === 'text' ? '文本文件' : '文件'} · ${formatBytes(payload.size)} · ${modeLabel}${payload.truncated ? ' · 已截断预览' : ''}`;
|
|
279
|
-
}
|
|
287
|
+
updatePreviewMeta();
|
|
280
288
|
if (!previewBodyNode) {
|
|
281
289
|
syncMarkdownToggleButton();
|
|
282
290
|
syncSaveButton();
|
|
@@ -315,8 +323,12 @@
|
|
|
315
323
|
doc: String(payload.content || ''),
|
|
316
324
|
language,
|
|
317
325
|
readOnly: state.previewReadOnly,
|
|
318
|
-
onChange: function () {
|
|
326
|
+
onChange: function (nextValue) {
|
|
319
327
|
state.previewDirty = true;
|
|
328
|
+
if (state.selectedFile) {
|
|
329
|
+
state.selectedFile.size = new TextEncoder().encode(nextValue).length;
|
|
330
|
+
}
|
|
331
|
+
updatePreviewMeta();
|
|
320
332
|
syncSaveButton();
|
|
321
333
|
}
|
|
322
334
|
});
|
|
@@ -364,6 +376,9 @@
|
|
|
364
376
|
if (mkdirBtn) {
|
|
365
377
|
mkdirBtn.disabled = state.loadingList || state.loadingFile || !state.sessionName || state.historyOnly === true;
|
|
366
378
|
}
|
|
379
|
+
if (newFileBtn) {
|
|
380
|
+
newFileBtn.disabled = state.loadingList || state.loadingFile || !state.sessionName || state.historyOnly === true;
|
|
381
|
+
}
|
|
367
382
|
if (!listNode) {
|
|
368
383
|
return;
|
|
369
384
|
}
|
|
@@ -596,6 +611,30 @@
|
|
|
596
611
|
}
|
|
597
612
|
}
|
|
598
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
|
+
|
|
599
638
|
function sync(context) {
|
|
600
639
|
const session = context && context.session;
|
|
601
640
|
const detail = context && context.detail;
|
|
@@ -669,6 +708,14 @@
|
|
|
669
708
|
});
|
|
670
709
|
}
|
|
671
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
|
+
|
|
672
719
|
if (saveBtn) {
|
|
673
720
|
saveBtn.addEventListener('click', function () {
|
|
674
721
|
saveCurrentFile().catch(function (e) {
|
|
@@ -679,6 +726,9 @@
|
|
|
679
726
|
|
|
680
727
|
if (toggleMarkdownViewBtn) {
|
|
681
728
|
toggleMarkdownViewBtn.addEventListener('click', function () {
|
|
729
|
+
if (state.editor && typeof state.editor.getValue === 'function' && state.selectedFile) {
|
|
730
|
+
state.selectedFile.content = state.editor.getValue();
|
|
731
|
+
}
|
|
682
732
|
state.markdownViewMode = state.markdownViewMode === 'source' ? 'rendered' : 'source';
|
|
683
733
|
renderPreviewPayload(state.selectedFile);
|
|
684
734
|
});
|
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$/),
|