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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "local-cmd-runner",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Run local shell commands from a web page",
5
5
  "homepage": "https://github.com/codewoow/local_cmd#readme",
6
6
  "bugs": {
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 = () => fmUploadInput.click();
232
- fmUploadInput.onchange = async (e) => {
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', currentRelPath);
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
- try {
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
- <pre class="preview-body" id="previewBody"></pre>
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
- const content = await fs.promises.readFile(targetPath, 'utf8');
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({ dest: os.tmpdir() });
84
- app.post('/cmd/api/upload', upload.single('file'), async (req, res) => {
85
- try {
86
- const targetDir = getSecurePath(req.body.path || '');
87
- const targetPath = path.join(targetDir, req.file.originalname);
88
- await fs.promises.rename(req.file.path, targetPath);
89
- res.json({ success: true });
90
- } catch (err) {
91
- res.status(500).json({ error: err.message });
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) => {