@wendongfly/myhi 1.2.0 → 1.3.1

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/dist/package.json CHANGED
@@ -1 +1 @@
1
- {"type":"module","version":"1.2.0"}
1
+ {"type":"module","version":"1.3.1"}
@@ -231,6 +231,66 @@
231
231
  z-index: 10;
232
232
  }
233
233
  #status-overlay.hidden { display: none; }
234
+
235
+ /* ── 文件管理面板 ── */
236
+ #file-panel-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 59; display: none; }
237
+ #file-panel-backdrop.open { display: block; }
238
+ #file-panel {
239
+ position: fixed; top: 0; right: -340px; width: 320px; max-width: 90vw; height: 100%;
240
+ background: #161b22; border-left: 1px solid #30363d; z-index: 60;
241
+ transition: right 0.25s ease; display: flex; flex-direction: column;
242
+ }
243
+ #file-panel.open { right: 0; }
244
+ .fp-header {
245
+ display: flex; align-items: center; padding: 0.75rem; border-bottom: 1px solid #30363d;
246
+ gap: 0.5rem; flex-shrink: 0;
247
+ }
248
+ .fp-header h3 { flex: 1; font-size: 0.9rem; margin: 0; color: #e6edf3; }
249
+ .fp-btn {
250
+ background: #21262d; color: #e6edf3; border: 1px solid #30363d; border-radius: 6px;
251
+ padding: 0.3rem 0.6rem; font-size: 0.75rem; cursor: pointer;
252
+ }
253
+ .fp-btn:active { background: #30363d; }
254
+ .fp-path {
255
+ padding: 0.4rem 0.75rem; font-size: 0.7rem; color: #8b949e;
256
+ border-bottom: 1px solid #21262d; word-break: break-all; flex-shrink: 0;
257
+ }
258
+ .fp-list { flex: 1; overflow-y: auto; padding: 0.25rem 0; }
259
+ .fp-item {
260
+ display: flex; align-items: center; padding: 0.5rem 0.75rem; gap: 0.5rem;
261
+ cursor: pointer; font-size: 0.82rem; color: #e6edf3;
262
+ }
263
+ .fp-item:active { background: #21262d; }
264
+ .fp-item .fp-icon { width: 1.2rem; text-align: center; flex-shrink: 0; }
265
+ .fp-item .fp-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
266
+ .fp-item .fp-size { color: #8b949e; font-size: 0.7rem; white-space: nowrap; }
267
+ .fp-item .fp-dl { color: #58a6ff; font-size: 0.7rem; text-decoration: none; padding: 0.2rem 0.4rem; }
268
+ .fp-empty { text-align: center; color: #8b949e; padding: 2rem 0.75rem; font-size: 0.85rem; }
269
+ .fp-drop-zone {
270
+ margin: 0.5rem 0.75rem; padding: 1.5rem; border: 2px dashed #30363d; border-radius: 8px;
271
+ text-align: center; color: #8b949e; font-size: 0.8rem; flex-shrink: 0;
272
+ transition: border-color 0.2s, background 0.2s;
273
+ }
274
+ .fp-drop-zone.dragover { border-color: #58a6ff; background: rgba(88,166,255,0.08); }
275
+ .fp-progress { padding: 0.4rem 0.75rem; font-size: 0.75rem; color: #d29922; flex-shrink: 0; display: none; }
276
+
277
+ /* ── 同步相关 ── */
278
+ .fp-sync-bar { display: flex; gap: 0.4rem; padding: 0.5rem 0.75rem; border-bottom: 1px solid #21262d; flex-shrink: 0; align-items: center; flex-wrap: wrap; }
279
+ .fp-sync-bar .fp-btn { font-size: 0.7rem; padding: 0.25rem 0.5rem; }
280
+ .fp-sync-bar .sync-dir { font-size: 0.68rem; color: #8b949e; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
281
+ .fp-diff-item { display: flex; align-items: center; padding: 0.4rem 0.75rem; gap: 0.4rem; font-size: 0.78rem; color: #e6edf3; }
282
+ .fp-diff-item .badge { font-size: 0.65rem; padding: 0.1rem 0.35rem; border-radius: 4px; font-weight: 600; flex-shrink: 0; }
283
+ .badge-new { background: #238636; color: #fff; }
284
+ .badge-changed { background: #9e6a03; color: #fff; }
285
+ .badge-arrow { color: #8b949e; flex-shrink: 0; font-size: 0.7rem; }
286
+ .fp-diff-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
287
+ .fp-diff-summary { padding: 0.75rem; text-align: center; color: #8b949e; font-size: 0.8rem; }
288
+ .fp-diff-actions { display: flex; gap: 0.5rem; padding: 0.5rem 0.75rem; flex-shrink: 0; }
289
+ .fp-diff-actions .fp-btn { flex: 1; text-align: center; padding: 0.45rem; }
290
+ .fp-btn-primary { background: #238636; border-color: #2ea04380; }
291
+ .fp-btn-primary:active { background: #2ea043; }
292
+ .fp-btn-warn { background: #9e6a03; border-color: #d2992280; }
293
+ .fp-btn-warn:active { background: #bb8009; }
234
294
  </style>
235
295
  </head>
236
296
  <body>
@@ -238,6 +298,7 @@
238
298
  <div id="top-bar">
239
299
  <div id="session-title">加载中...</div>
240
300
  <span id="viewer-count"></span>
301
+ <button class="top-btn" onclick="openFilePanel()">文件</button>
241
302
  <button class="top-btn" onclick="showQR()">扫码分享</button>
242
303
  <button class="top-btn" onclick="goBack()">← 返回</button>
243
304
  <button class="top-btn" id="logout-btn" onclick="doLogout()" style="display:none;color:#f0883e">退出</button>
@@ -269,6 +330,31 @@
269
330
  <button class="sk" data-send="alt-.">Alt+.</button>
270
331
  </div>
271
332
 
333
+ <!-- 文件管理面板 -->
334
+ <div id="file-panel-backdrop" onclick="closeFilePanel()"></div>
335
+ <div id="file-panel">
336
+ <div class="fp-header">
337
+ <h3 id="fp-title">文件管理</h3>
338
+ <button class="fp-btn" onclick="fpUploadClick()">上传</button>
339
+ <button class="fp-btn" onclick="fpStartSync()">同步</button>
340
+ <button class="fp-btn" onclick="closeFilePanel()">关闭</button>
341
+ </div>
342
+ <div class="fp-sync-bar" id="fp-sync-bar" style="display:none">
343
+ <span class="sync-dir" id="fp-sync-dir"></span>
344
+ <button class="fp-btn" onclick="fpPickDir()">换目录</button>
345
+ <button class="fp-btn" onclick="fpExitSync()">退出同步</button>
346
+ </div>
347
+ <div class="fp-path" id="fp-path"></div>
348
+ <div class="fp-list" id="fp-list"></div>
349
+ <div class="fp-diff-actions" id="fp-diff-actions" style="display:none">
350
+ <button class="fp-btn fp-btn-primary" onclick="fpSyncToLocal()">下载到本地</button>
351
+ <button class="fp-btn fp-btn-warn" onclick="fpSyncToServer()">上传到服务器</button>
352
+ </div>
353
+ <div class="fp-progress" id="fp-progress"></div>
354
+ <div class="fp-drop-zone" id="fp-drop">拖拽文件到此处上传,或点击上方按钮</div>
355
+ <input type="file" id="fp-file-input" multiple style="display:none">
356
+ </div>
357
+
272
358
  <!-- QR modal -->
273
359
  <div id="qr-modal">
274
360
  <div id="qr-box">
@@ -387,8 +473,8 @@
387
473
 
388
474
  socket.on('disconnect', () => {
389
475
  // 显示断连提示
390
- term.write('
391
- [myhi] 连接已断开,正在重连...
476
+ term.write('
477
+ [myhi] 连接已断开,正在重连...
392
478
  ');
393
479
  });
394
480
 
@@ -653,9 +739,374 @@
653
739
  input.click();
654
740
  }
655
741
 
742
+ // ── 文件管理 ──────────────────────────────────────
743
+ let fpCurrentPath = '';
744
+ let fpSessionId = null;
745
+
746
+ function openFilePanel() {
747
+ const tab = tabs.find(t => t.id === activeTabId);
748
+ if (!tab) return;
749
+ fpSessionId = tab.sessionId;
750
+ fpCurrentPath = '';
751
+ loadFiles();
752
+ document.getElementById('file-panel').classList.add('open');
753
+ document.getElementById('file-panel-backdrop').classList.add('open');
754
+ }
755
+ function closeFilePanel() {
756
+ document.getElementById('file-panel').classList.remove('open');
757
+ document.getElementById('file-panel-backdrop').classList.remove('open');
758
+ }
759
+
760
+ async function loadFiles() {
761
+ if (!fpSessionId) return;
762
+ const list = document.getElementById('fp-list');
763
+ list.innerHTML = '<div class="fp-empty">加载中...</div>';
764
+ try {
765
+ const r = await fetch(`/api/files/${fpSessionId}?path=${encodeURIComponent(fpCurrentPath)}`);
766
+ const data = await r.json();
767
+ if (!data.ok) { list.innerHTML = `<div class="fp-empty">${esc(data.error)}</div>`; return; }
768
+ document.getElementById('fp-path').textContent = data.cwd + (data.path ? '/' + data.path : '');
769
+ if (!data.entries.length && data.parent == null) {
770
+ list.innerHTML = '<div class="fp-empty">空目录</div>';
771
+ return;
772
+ }
773
+ let html = '';
774
+ if (data.parent != null) {
775
+ html += `<div class="fp-item" onclick="fpNav('${esc(data.parent)}')"><span class="fp-icon">📁</span><span class="fp-name">..</span></div>`;
776
+ }
777
+ for (const e of data.entries) {
778
+ const rel = fpCurrentPath ? fpCurrentPath + '/' + e.name : e.name;
779
+ if (e.isDir) {
780
+ html += `<div class="fp-item" onclick="fpNav('${esc(rel)}')"><span class="fp-icon">📁</span><span class="fp-name">${esc(e.name)}</span></div>`;
781
+ } else {
782
+ html += `<div class="fp-item">
783
+ <span class="fp-icon">📄</span>
784
+ <span class="fp-name">${esc(e.name)}</span>
785
+ <span class="fp-size">${formatSize(e.size)}</span>
786
+ <a class="fp-dl" href="/api/files/${fpSessionId}/download?path=${encodeURIComponent(rel)}" download title="下载">下载</a>
787
+ </div>`;
788
+ }
789
+ }
790
+ list.innerHTML = html;
791
+ } catch (e) {
792
+ list.innerHTML = `<div class="fp-empty">加载失败: ${esc(e.message)}</div>`;
793
+ }
794
+ }
795
+
796
+ function fpNav(path) {
797
+ fpCurrentPath = path;
798
+ loadFiles();
799
+ }
800
+
801
+ function formatSize(bytes) {
802
+ if (bytes == null) return '';
803
+ if (bytes < 1024) return bytes + ' B';
804
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
805
+ if (bytes < 1024 * 1024 * 1024) return (bytes / 1024 / 1024).toFixed(1) + ' MB';
806
+ return (bytes / 1024 / 1024 / 1024).toFixed(1) + ' GB';
807
+ }
808
+
809
+ function fpUploadClick() {
810
+ document.getElementById('fp-file-input').click();
811
+ }
812
+
813
+ document.getElementById('fp-file-input').addEventListener('change', (e) => {
814
+ if (e.target.files.length) fpUploadFiles(e.target.files);
815
+ e.target.value = '';
816
+ });
817
+
818
+ async function fpUploadFiles(files) {
819
+ if (!fpSessionId || !files.length) return;
820
+ const progress = document.getElementById('fp-progress');
821
+ progress.style.display = 'block';
822
+ progress.textContent = `正在上传 ${files.length} 个文件...`;
823
+ try {
824
+ const form = new FormData();
825
+ for (const f of files) form.append('files', f, f.webkitRelativePath || f.name);
826
+ const r = await fetch(`/api/files/${fpSessionId}/upload?path=${encodeURIComponent(fpCurrentPath)}`, {
827
+ method: 'POST', body: form,
828
+ });
829
+ const data = await r.json();
830
+ if (data.ok) {
831
+ progress.textContent = `已上传 ${data.files.length} 个文件`;
832
+ loadFiles();
833
+ } else {
834
+ progress.textContent = '上传失败: ' + (data.error || '未知错误');
835
+ }
836
+ } catch (e) {
837
+ progress.textContent = '上传出错: ' + e.message;
838
+ }
839
+ setTimeout(() => { progress.style.display = 'none'; }, 3000);
840
+ }
841
+
842
+ // 拖拽上传
843
+ const fpDrop = document.getElementById('fp-drop');
844
+ fpDrop.addEventListener('dragover', (e) => { e.preventDefault(); fpDrop.classList.add('dragover'); });
845
+ fpDrop.addEventListener('dragleave', () => fpDrop.classList.remove('dragover'));
846
+ fpDrop.addEventListener('drop', (e) => {
847
+ e.preventDefault();
848
+ fpDrop.classList.remove('dragover');
849
+ if (e.dataTransfer.files.length) fpUploadFiles(e.dataTransfer.files);
850
+ });
851
+
852
+ // ── 文件同步(File System Access API)──────────
853
+ let syncDirHandle = null;
854
+ let syncMode = false;
855
+ let syncDiff = { toDownload: [], toUpload: [] };
856
+
857
+ async function fpStartSync() {
858
+ if (!window.showDirectoryPicker) {
859
+ alert('当前浏览器不支持文件夹访问,请使用 Chrome 或 Edge');
860
+ return;
861
+ }
862
+ if (!syncDirHandle) {
863
+ await fpPickDir();
864
+ if (!syncDirHandle) return;
865
+ }
866
+ syncMode = true;
867
+ document.getElementById('fp-sync-bar').style.display = 'flex';
868
+ document.getElementById('fp-drop').style.display = 'none';
869
+ await fpComputeDiff();
870
+ }
871
+
872
+ async function fpPickDir() {
873
+ try {
874
+ syncDirHandle = await window.showDirectoryPicker({ mode: 'readwrite' });
875
+ document.getElementById('fp-sync-dir').textContent = syncDirHandle.name;
876
+ if (syncMode) await fpComputeDiff();
877
+ } catch (e) {
878
+ if (e.name !== 'AbortError') alert('选择文件夹失败: ' + e.message);
879
+ }
880
+ }
881
+
882
+ function fpExitSync() {
883
+ syncMode = false;
884
+ document.getElementById('fp-sync-bar').style.display = 'none';
885
+ document.getElementById('fp-diff-actions').style.display = 'none';
886
+ document.getElementById('fp-drop').style.display = '';
887
+ loadFiles();
888
+ }
889
+
890
+ // 递归遍历本地目录
891
+ async function walkLocalDir(dirHandle, prefix) {
892
+ const files = [];
893
+ for await (const [name, handle] of dirHandle.entries()) {
894
+ if (name.startsWith('.')) continue;
895
+ const rel = prefix ? prefix + '/' + name : name;
896
+ if (handle.kind === 'directory') {
897
+ files.push(...await walkLocalDir(handle, rel));
898
+ } else {
899
+ const file = await handle.getFile();
900
+ files.push({ name: rel, size: file.size, handle, dirHandle });
901
+ }
902
+ }
903
+ return files;
904
+ }
905
+
906
+ // 计算文件 SHA-256(与服务端一致)
907
+ async function hashFile(file) {
908
+ const buf = await file.arrayBuffer();
909
+ const hash = await crypto.subtle.digest('SHA-256', buf);
910
+ return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('');
911
+ }
912
+
913
+ // 比对差异
914
+ async function fpComputeDiff() {
915
+ if (!fpSessionId || !syncDirHandle) return;
916
+ const list = document.getElementById('fp-list');
917
+ list.innerHTML = '<div class="fp-empty">正在比对文件...</div>';
918
+ const progress = document.getElementById('fp-progress');
919
+ progress.style.display = 'block';
920
+
921
+ try {
922
+ // 获取服务端清单
923
+ progress.textContent = '获取服务器文件清单...';
924
+ const r = await fetch(`/api/files/${fpSessionId}/manifest?path=${encodeURIComponent(fpCurrentPath)}`);
925
+ const manifest = await r.json();
926
+ if (!manifest.ok) throw new Error(manifest.error);
927
+
928
+ // 遍历本地目录
929
+ progress.textContent = '扫描本地文件...';
930
+ const localFiles = await walkLocalDir(syncDirHandle, '');
931
+
932
+ const serverMap = new Map(manifest.files.map(f => [f.name, f]));
933
+ const localMap = new Map(localFiles.map(f => [f.name, f]));
934
+
935
+ const toDownload = []; // 服务器有而本地没有,或内容不同
936
+ const toUpload = []; // 本地有而服务器没有,或内容不同
937
+
938
+ // 找需要下载的(服务器→本地)
939
+ let checked = 0;
940
+ for (const [name, sf] of serverMap) {
941
+ checked++;
942
+ if (checked % 20 === 0) progress.textContent = `比对中... ${checked}/${serverMap.size + localMap.size}`;
943
+ const lf = localMap.get(name);
944
+ if (!lf) {
945
+ toDownload.push({ name, size: sf.size, reason: 'new' });
946
+ } else if (lf.size !== sf.size) {
947
+ toDownload.push({ name, size: sf.size, reason: 'changed' });
948
+ } else if (sf.hash && sf.size < 10 * 1024 * 1024) {
949
+ // 大小相同但可能内容不同,比对哈希(仅 <10MB)
950
+ const file = await lf.handle.getFile();
951
+ const localHash = await hashFile(file);
952
+ if (localHash !== sf.hash) {
953
+ toDownload.push({ name, size: sf.size, reason: 'changed' });
954
+ }
955
+ }
956
+ }
957
+
958
+ // 找需要上传的(本地→服务器)
959
+ for (const [name, lf] of localMap) {
960
+ checked++;
961
+ if (checked % 20 === 0) progress.textContent = `比对中... ${checked}/${serverMap.size + localMap.size}`;
962
+ const sf = serverMap.get(name);
963
+ if (!sf) {
964
+ toUpload.push({ name, size: lf.size, reason: 'new', handle: lf.handle });
965
+ } else if (lf.size !== sf.size) {
966
+ toUpload.push({ name, size: lf.size, reason: 'changed', handle: lf.handle });
967
+ } else if (sf.hash && lf.size < 10 * 1024 * 1024) {
968
+ const file = await lf.handle.getFile();
969
+ const localHash = await hashFile(file);
970
+ if (localHash !== sf.hash) {
971
+ toUpload.push({ name, size: lf.size, reason: 'changed', handle: lf.handle });
972
+ }
973
+ }
974
+ }
975
+
976
+ syncDiff = { toDownload, toUpload };
977
+ fpRenderDiff();
978
+ } catch (e) {
979
+ list.innerHTML = `<div class="fp-empty">比对失败: ${esc(e.message)}</div>`;
980
+ }
981
+ progress.style.display = 'none';
982
+ }
983
+
984
+ function fpRenderDiff() {
985
+ const { toDownload, toUpload } = syncDiff;
986
+ const list = document.getElementById('fp-list');
987
+
988
+ if (!toDownload.length && !toUpload.length) {
989
+ list.innerHTML = '<div class="fp-diff-summary">文件已同步,无差异</div>';
990
+ document.getElementById('fp-diff-actions').style.display = 'none';
991
+ return;
992
+ }
993
+
994
+ let html = '<div class="fp-diff-summary">';
995
+ if (toDownload.length) html += `${toDownload.length} 个文件需下载到本地`;
996
+ if (toDownload.length && toUpload.length) html += ',';
997
+ if (toUpload.length) html += `${toUpload.length} 个文件需上传到服务器`;
998
+ html += '</div>';
999
+
1000
+ for (const f of toDownload) {
1001
+ html += `<div class="fp-diff-item">
1002
+ <span class="badge ${f.reason === 'new' ? 'badge-new' : 'badge-changed'}">${f.reason === 'new' ? '新' : '改'}</span>
1003
+ <span class="badge-arrow">↓</span>
1004
+ <span class="fp-diff-name">${esc(f.name)}</span>
1005
+ <span class="fp-size">${formatSize(f.size)}</span>
1006
+ </div>`;
1007
+ }
1008
+ for (const f of toUpload) {
1009
+ html += `<div class="fp-diff-item">
1010
+ <span class="badge ${f.reason === 'new' ? 'badge-new' : 'badge-changed'}">${f.reason === 'new' ? '新' : '改'}</span>
1011
+ <span class="badge-arrow">↑</span>
1012
+ <span class="fp-diff-name">${esc(f.name)}</span>
1013
+ <span class="fp-size">${formatSize(f.size)}</span>
1014
+ </div>`;
1015
+ }
1016
+
1017
+ list.innerHTML = html;
1018
+ document.getElementById('fp-diff-actions').style.display = toDownload.length || toUpload.length ? 'flex' : 'none';
1019
+ }
1020
+
1021
+ // 同步到本地:从服务器下载差异文件写入本地目录
1022
+ async function fpSyncToLocal() {
1023
+ const { toDownload } = syncDiff;
1024
+ if (!toDownload.length) return;
1025
+ const progress = document.getElementById('fp-progress');
1026
+ progress.style.display = 'block';
1027
+
1028
+ let done = 0;
1029
+ for (const f of toDownload) {
1030
+ progress.textContent = `下载中 ${++done}/${toDownload.length}: ${f.name}`;
1031
+ try {
1032
+ const relPath = fpCurrentPath ? fpCurrentPath + '/' + f.name : f.name;
1033
+ const r = await fetch(`/api/files/${fpSessionId}/download?path=${encodeURIComponent(relPath)}`);
1034
+ if (!r.ok) continue;
1035
+ const blob = await r.blob();
1036
+ // 确保子目录存在并写入文件
1037
+ const parts = f.name.split('/');
1038
+ let dir = syncDirHandle;
1039
+ for (let i = 0; i < parts.length - 1; i++) {
1040
+ dir = await dir.getDirectoryHandle(parts[i], { create: true });
1041
+ }
1042
+ const fileHandle = await dir.getFileHandle(parts[parts.length - 1], { create: true });
1043
+ const writable = await fileHandle.createWritable();
1044
+ await writable.write(blob);
1045
+ await writable.close();
1046
+ } catch (e) {
1047
+ console.error(`同步下载失败 ${f.name}:`, e);
1048
+ }
1049
+ }
1050
+
1051
+ progress.textContent = `已下载 ${done} 个文件到本地`;
1052
+ setTimeout(() => { progress.style.display = 'none'; }, 3000);
1053
+ await fpComputeDiff(); // 重新比对
1054
+ }
1055
+
1056
+ // 同步到服务器:将本地差异文件上传
1057
+ async function fpSyncToServer() {
1058
+ const { toUpload } = syncDiff;
1059
+ if (!toUpload.length) return;
1060
+ const progress = document.getElementById('fp-progress');
1061
+ progress.style.display = 'block';
1062
+
1063
+ // 分批上传,每批最多 20 个文件
1064
+ const BATCH = 20;
1065
+ let done = 0;
1066
+ for (let i = 0; i < toUpload.length; i += BATCH) {
1067
+ const batch = toUpload.slice(i, i + BATCH);
1068
+ progress.textContent = `上传中 ${done}/${toUpload.length}...`;
1069
+ const form = new FormData();
1070
+ for (const f of batch) {
1071
+ try {
1072
+ const file = await f.handle.getFile();
1073
+ form.append('files', file, f.name);
1074
+ } catch (e) {
1075
+ console.error(`读取本地文件失败 ${f.name}:`, e);
1076
+ }
1077
+ }
1078
+ try {
1079
+ await fetch(`/api/files/${fpSessionId}/upload?path=${encodeURIComponent(fpCurrentPath)}`, {
1080
+ method: 'POST', body: form,
1081
+ });
1082
+ } catch (e) {
1083
+ console.error('上传批次失败:', e);
1084
+ }
1085
+ done += batch.length;
1086
+ }
1087
+
1088
+ progress.textContent = `已上传 ${done} 个文件到服务器`;
1089
+ setTimeout(() => { progress.style.display = 'none'; }, 3000);
1090
+ await fpComputeDiff(); // 重新比对
1091
+ }
1092
+
1093
+ // 监听文件变更通知自动刷新
1094
+ // 使用第一个 tab 的 socket 监听全局事件
1095
+ function setupFileChangeListener() {
1096
+ const tab = tabs[0];
1097
+ if (!tab) return;
1098
+ tab.socket.on('files:changed', ({ sessionId }) => {
1099
+ if (sessionId === fpSessionId && document.getElementById('file-panel').classList.contains('open')) {
1100
+ if (syncMode) fpComputeDiff();
1101
+ else loadFiles();
1102
+ }
1103
+ });
1104
+ }
1105
+
656
1106
  // ── 初始化:打开 URL 中指定的会话 ──
657
1107
  const initTab = createTab(INIT_SESSION_ID);
658
1108
  switchTab(initTab.id);
1109
+ setupFileChangeListener();
659
1110
  </script>
660
1111
  </body>
661
1112
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wendongfly/myhi",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "description": "Web-based terminal sharing with chat UI — control your terminal from phone via LAN/Tailscale",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -37,7 +37,7 @@
37
37
  "node": ">=18.0.0"
38
38
  },
39
39
  "dependencies": {
40
- "node-pty": "^1.0.0",
40
+ "node-pty": "^1.0.0",
41
41
  "socket.io-client": "^4.8.3"
42
42
  },
43
43
  "devDependencies": {