local-cmd-runner 1.0.4 → 1.0.6
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/package.json +1 -1
- package/public/app.js +99 -19
- package/public/index.html +8 -1
- package/public/style.css +37 -0
- package/server.js +24 -12
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -146,6 +146,9 @@ const fmCurrentPath = document.getElementById('fmCurrentPath');
|
|
|
146
146
|
const fmUploadBtn = document.getElementById('fmUploadBtn');
|
|
147
147
|
const fmUploadInput = document.getElementById('fmUploadInput');
|
|
148
148
|
const fmRefreshBtn = document.getElementById('fmRefreshBtn');
|
|
149
|
+
const fmUploadProgress = document.getElementById('fmUploadProgress');
|
|
150
|
+
const fmProgressBar = document.getElementById('fmProgressBar');
|
|
151
|
+
const fmProgressText = document.getElementById('fmProgressText');
|
|
149
152
|
|
|
150
153
|
const previewModal = document.getElementById('previewModal');
|
|
151
154
|
const previewTitle = document.getElementById('previewTitle');
|
|
@@ -206,6 +209,14 @@ function renderTree(items, relPath) {
|
|
|
206
209
|
|
|
207
210
|
if (item.isDirectory) {
|
|
208
211
|
el.onclick = () => loadTree(item.path);
|
|
212
|
+
|
|
213
|
+
const uploadDirBtn = document.createElement('button');
|
|
214
|
+
uploadDirBtn.innerHTML = '上传到此';
|
|
215
|
+
uploadDirBtn.onclick = (e) => {
|
|
216
|
+
e.stopPropagation();
|
|
217
|
+
triggerDirectoryUpload(item.path);
|
|
218
|
+
};
|
|
219
|
+
actions.appendChild(uploadDirBtn);
|
|
209
220
|
} else {
|
|
210
221
|
const previewBtn = document.createElement('button');
|
|
211
222
|
previewBtn.innerHTML = '查看';
|
|
@@ -226,43 +237,112 @@ function renderTree(items, relPath) {
|
|
|
226
237
|
});
|
|
227
238
|
}
|
|
228
239
|
|
|
240
|
+
let pendingUploadPath = '';
|
|
241
|
+
function triggerDirectoryUpload(targetPath) {
|
|
242
|
+
pendingUploadPath = targetPath;
|
|
243
|
+
fmUploadInput.click();
|
|
244
|
+
}
|
|
245
|
+
|
|
229
246
|
if (fmRefreshBtn) {
|
|
230
247
|
fmRefreshBtn.onclick = () => loadTree(currentRelPath);
|
|
231
|
-
fmUploadBtn.onclick = () =>
|
|
232
|
-
fmUploadInput.onchange =
|
|
248
|
+
fmUploadBtn.onclick = () => triggerDirectoryUpload(currentRelPath);
|
|
249
|
+
fmUploadInput.onchange = (e) => {
|
|
233
250
|
const file = e.target.files[0];
|
|
234
251
|
if (!file) return;
|
|
252
|
+
|
|
253
|
+
if (file.size > 5 * 1024 * 1024) {
|
|
254
|
+
alert('上传失败:文件大小不能超过 5MB!');
|
|
255
|
+
fmUploadInput.value = '';
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
235
259
|
const formData = new FormData();
|
|
236
260
|
formData.append('file', file);
|
|
237
|
-
formData.append('path',
|
|
261
|
+
formData.append('path', pendingUploadPath);
|
|
262
|
+
|
|
263
|
+
fmUploadBtn.textContent = '...';
|
|
264
|
+
if (fmUploadProgress) {
|
|
265
|
+
fmUploadProgress.classList.remove('hidden');
|
|
266
|
+
fmProgressBar.style.width = '0%';
|
|
267
|
+
fmProgressText.textContent = '0%';
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const xhr = new XMLHttpRequest();
|
|
271
|
+
xhr.open('POST', '/cmd/api/upload');
|
|
272
|
+
|
|
273
|
+
xhr.upload.onprogress = (event) => {
|
|
274
|
+
if (event.lengthComputable && fmUploadProgress) {
|
|
275
|
+
const percentComplete = Math.floor((event.loaded / event.total) * 100);
|
|
276
|
+
fmProgressBar.style.width = percentComplete + '%';
|
|
277
|
+
fmProgressText.textContent = percentComplete + '%';
|
|
278
|
+
}
|
|
279
|
+
};
|
|
238
280
|
|
|
239
|
-
|
|
240
|
-
fmUploadBtn.textContent = '...';
|
|
241
|
-
const res = await fetch('/cmd/api/upload', {
|
|
242
|
-
method: 'POST',
|
|
243
|
-
body: formData
|
|
244
|
-
});
|
|
245
|
-
if (!res.ok) throw new Error('上传时服务器报错');
|
|
281
|
+
xhr.onload = () => {
|
|
246
282
|
fmUploadInput.value = '';
|
|
247
|
-
loadTree(currentRelPath);
|
|
248
|
-
} catch (err) {
|
|
249
|
-
alert('上传失败: ' + err.message);
|
|
250
|
-
} finally {
|
|
251
283
|
fmUploadBtn.textContent = '↑';
|
|
252
|
-
|
|
284
|
+
if (fmUploadProgress) fmUploadProgress.classList.add('hidden');
|
|
285
|
+
|
|
286
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
287
|
+
// Success
|
|
288
|
+
loadTree(currentRelPath);
|
|
289
|
+
} else {
|
|
290
|
+
let errMsg = '上传时服务器报错';
|
|
291
|
+
try { errMsg = JSON.parse(xhr.responseText).error || errMsg; } catch (e) {}
|
|
292
|
+
alert('上传失败: ' + errMsg);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
xhr.onerror = () => {
|
|
297
|
+
fmUploadInput.value = '';
|
|
298
|
+
fmUploadBtn.textContent = '↑';
|
|
299
|
+
if (fmUploadProgress) fmUploadProgress.classList.add('hidden');
|
|
300
|
+
alert('上传失败: 网络错误');
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
xhr.send(formData);
|
|
253
304
|
};
|
|
254
305
|
}
|
|
255
306
|
|
|
256
307
|
async function previewFile(path) {
|
|
257
308
|
try {
|
|
258
|
-
const res = await fetch(`/cmd/api/preview?path=${encodeURIComponent(path)}`);
|
|
259
|
-
if (!res.ok) throw new Error('获取预览失败');
|
|
260
|
-
const text = await res.text();
|
|
261
309
|
previewTitle.textContent = path;
|
|
262
|
-
previewBody.textContent = text;
|
|
263
310
|
previewModal.classList.remove('hidden');
|
|
311
|
+
previewBody.innerHTML = '<div style="margin: 20px;">加载中...</div>';
|
|
312
|
+
|
|
313
|
+
const ext = path.split('.').pop().toLowerCase();
|
|
314
|
+
const isImage = ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'ico'].includes(ext);
|
|
315
|
+
const isHtml = ['html', 'htm'].includes(ext);
|
|
316
|
+
const isVideo = ['mp4', 'webm', 'ogg'].includes(ext);
|
|
317
|
+
const isAudio = ['mp3', 'wav', 'ogg'].includes(ext);
|
|
318
|
+
|
|
319
|
+
const fileUrl = `/cmd/api/preview?path=${encodeURIComponent(path)}`;
|
|
320
|
+
|
|
321
|
+
if (isImage) {
|
|
322
|
+
previewBody.innerHTML = `<img src="${fileUrl}" style="max-width: 100%; max-height: 70vh; object-fit: contain; border-radius: 4px;" />`;
|
|
323
|
+
} else if (isVideo) {
|
|
324
|
+
previewBody.innerHTML = `<video src="${fileUrl}" controls style="max-width: 100%; max-height: 70vh; border-radius: 4px;"></video>`;
|
|
325
|
+
} else if (isAudio) {
|
|
326
|
+
previewBody.innerHTML = `<audio src="${fileUrl}" controls style="width: 100%; margin-top: 20px;"></audio>`;
|
|
327
|
+
} else if (isHtml) {
|
|
328
|
+
previewBody.innerHTML = `<iframe src="${fileUrl}" style="width: 100%; min-height: 60vh; border: none; background: white; border-radius: 4px;"></iframe>`;
|
|
329
|
+
} else {
|
|
330
|
+
const res = await fetch(fileUrl);
|
|
331
|
+
if (!res.ok) throw new Error('获取预览失败');
|
|
332
|
+
const text = await res.text();
|
|
333
|
+
previewBody.innerHTML = '';
|
|
334
|
+
const pre = document.createElement('pre');
|
|
335
|
+
pre.textContent = text;
|
|
336
|
+
pre.style.whiteSpace = 'pre-wrap';
|
|
337
|
+
pre.style.wordBreak = 'break-all';
|
|
338
|
+
pre.style.margin = '0';
|
|
339
|
+
pre.style.fontFamily = "'Fira Code', monospace";
|
|
340
|
+
pre.style.width = "100%";
|
|
341
|
+
previewBody.appendChild(pre);
|
|
342
|
+
}
|
|
264
343
|
} catch (err) {
|
|
265
344
|
alert('预览失败: ' + err.message);
|
|
345
|
+
previewModal.classList.add('hidden');
|
|
266
346
|
}
|
|
267
347
|
}
|
|
268
348
|
|
package/public/index.html
CHANGED
|
@@ -58,6 +58,12 @@
|
|
|
58
58
|
</div>
|
|
59
59
|
</div>
|
|
60
60
|
<div class="fm-path" id="fmCurrentPath">/root/.openclaw</div>
|
|
61
|
+
<div id="fmUploadProgress" class="fm-upload-progress hidden">
|
|
62
|
+
<div class="fm-progress-bar-wrap">
|
|
63
|
+
<div class="fm-progress-bar" id="fmProgressBar"></div>
|
|
64
|
+
</div>
|
|
65
|
+
<div class="fm-progress-text" id="fmProgressText">0%</div>
|
|
66
|
+
</div>
|
|
61
67
|
<div class="fm-body" id="fmTree">
|
|
62
68
|
<div class="fm-loading">加载中...</div>
|
|
63
69
|
</div>
|
|
@@ -68,7 +74,8 @@
|
|
|
68
74
|
<div id="previewModal" class="modal-overlay hidden">
|
|
69
75
|
<div class="modal-content preview-modal-content">
|
|
70
76
|
<h3 id="previewTitle" style="word-break: break-all;">文件预览</h3>
|
|
71
|
-
<
|
|
77
|
+
<div class="preview-body" id="previewBody"
|
|
78
|
+
style="display: flex; flex-direction: column; align-items: center; justify-content: center;"></div>
|
|
72
79
|
<div class="modal-actions">
|
|
73
80
|
<button id="previewClose" class="btn-primary">关闭</button>
|
|
74
81
|
</div>
|
package/public/style.css
CHANGED
|
@@ -470,3 +470,40 @@ input[type="text"]:focus {
|
|
|
470
470
|
white-space: pre-wrap;
|
|
471
471
|
word-break: break-all;
|
|
472
472
|
}
|
|
473
|
+
|
|
474
|
+
/* File Manager Progress */
|
|
475
|
+
.fm-upload-progress {
|
|
476
|
+
display: flex;
|
|
477
|
+
align-items: center;
|
|
478
|
+
padding: 8px 16px;
|
|
479
|
+
background: rgba(59, 130, 246, 0.1);
|
|
480
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
|
481
|
+
gap: 12px;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
.fm-upload-progress.hidden {
|
|
485
|
+
display: none;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
.fm-progress-bar-wrap {
|
|
489
|
+
flex: 1;
|
|
490
|
+
background: rgba(0, 0, 0, 0.3);
|
|
491
|
+
border-radius: 4px;
|
|
492
|
+
height: 8px;
|
|
493
|
+
overflow: hidden;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
.fm-progress-bar {
|
|
497
|
+
background: var(--accent);
|
|
498
|
+
width: 0%;
|
|
499
|
+
height: 100%;
|
|
500
|
+
transition: width 0.1s linear;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
.fm-progress-text {
|
|
504
|
+
font-size: 0.8rem;
|
|
505
|
+
color: var(--text-primary);
|
|
506
|
+
min-width: 36px;
|
|
507
|
+
text-align: right;
|
|
508
|
+
font-variant-numeric: tabular-nums;
|
|
509
|
+
}
|
package/server.js
CHANGED
|
@@ -64,8 +64,7 @@ app.get('/cmd/api/tree', async (req, res) => {
|
|
|
64
64
|
app.get('/cmd/api/preview', async (req, res) => {
|
|
65
65
|
try {
|
|
66
66
|
const targetPath = getSecurePath(req.query.path);
|
|
67
|
-
|
|
68
|
-
res.send(content);
|
|
67
|
+
res.sendFile(targetPath);
|
|
69
68
|
} catch (err) {
|
|
70
69
|
res.status(500).send(err.message);
|
|
71
70
|
}
|
|
@@ -80,16 +79,29 @@ app.get('/cmd/api/download', (req, res) => {
|
|
|
80
79
|
}
|
|
81
80
|
});
|
|
82
81
|
|
|
83
|
-
const upload = multer({
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
82
|
+
const upload = multer({
|
|
83
|
+
dest: os.tmpdir(),
|
|
84
|
+
limits: { fileSize: 5 * 1024 * 1024 }
|
|
85
|
+
}).single('file');
|
|
86
|
+
|
|
87
|
+
app.post('/cmd/api/upload', (req, res) => {
|
|
88
|
+
upload(req, res, async (err) => {
|
|
89
|
+
if (err instanceof multer.MulterError && err.code === 'LIMIT_FILE_SIZE') {
|
|
90
|
+
return res.status(400).json({ error: '文件大小超出 5MB 限制!' });
|
|
91
|
+
} else if (err) {
|
|
92
|
+
return res.status(500).json({ error: err.message });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
if (!req.file) throw new Error('未提供文件');
|
|
97
|
+
const targetDir = getSecurePath(req.body.path || '');
|
|
98
|
+
const targetPath = path.join(targetDir, req.file.originalname);
|
|
99
|
+
await fs.promises.rename(req.file.path, targetPath);
|
|
100
|
+
res.json({ success: true });
|
|
101
|
+
} catch (e) {
|
|
102
|
+
res.status(500).json({ error: e.message });
|
|
103
|
+
}
|
|
104
|
+
});
|
|
93
105
|
});
|
|
94
106
|
|
|
95
107
|
io.on('connection', (socket) => {
|