@wendongfly/zihi 1.1.4 → 1.1.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/dist/chat.html CHANGED
@@ -203,6 +203,52 @@
203
203
  .setting-toggle input[type=checkbox]::after { content: ''; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; background: #fff; border-radius: 50%; transition: left 0.2s; }
204
204
  .setting-toggle input[type=checkbox]:checked::after { left: 18px; }
205
205
 
206
+ /* ── 文件管理面板 ─────────────────────── */
207
+ #file-panel-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 62; display: none; }
208
+ #file-panel-backdrop.open { display: block; }
209
+ #file-panel {
210
+ position: fixed; top: 0; right: -340px; width: 320px; max-width: 90vw; height: 100%;
211
+ background: #161b22; border-left: 1px solid #30363d; z-index: 63;
212
+ transition: right 0.25s ease; display: flex; flex-direction: column;
213
+ }
214
+ #file-panel.open { right: 0; }
215
+ .fp-header { display: flex; align-items: center; padding: 0.75rem; border-bottom: 1px solid #30363d; gap: 0.5rem; flex-shrink: 0; }
216
+ .fp-header h3 { flex: 1; font-size: 0.9rem; margin: 0; color: #e6edf3; }
217
+ .fp-btn { background: #21262d; color: #e6edf3; border: 1px solid #30363d; border-radius: 6px; padding: 0.3rem 0.6rem; font-size: 0.75rem; cursor: pointer; }
218
+ .fp-btn:active { background: #30363d; }
219
+ .fp-path { padding: 0.4rem 0.75rem; font-size: 0.7rem; color: #8b949e; border-bottom: 1px solid #21262d; word-break: break-all; flex-shrink: 0; }
220
+ .fp-list { flex: 1; overflow-y: auto; padding: 0.25rem 0; }
221
+ .fp-item { display: flex; align-items: center; padding: 0.5rem 0.75rem; gap: 0.5rem; cursor: pointer; font-size: 0.82rem; color: #e6edf3; }
222
+ .fp-item:active { background: #21262d; }
223
+ .fp-item .fp-icon { width: 1.2rem; text-align: center; flex-shrink: 0; }
224
+ .fp-item .fp-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
225
+ .fp-item .fp-size { color: #8b949e; font-size: 0.7rem; white-space: nowrap; }
226
+ .fp-item .fp-dl { color: #58a6ff; font-size: 0.7rem; text-decoration: none; padding: 0.2rem 0.4rem; }
227
+ .fp-empty { text-align: center; color: #8b949e; padding: 2rem 0.75rem; font-size: 0.85rem; }
228
+ .fp-drop-zone {
229
+ margin: 0.5rem 0.75rem; padding: 1.5rem; border: 2px dashed #30363d; border-radius: 8px;
230
+ text-align: center; color: #8b949e; font-size: 0.8rem; flex-shrink: 0;
231
+ transition: border-color 0.2s, background 0.2s;
232
+ }
233
+ .fp-drop-zone.dragover { border-color: #58a6ff; background: rgba(88,166,255,0.08); }
234
+ .fp-progress { padding: 0.4rem 0.75rem; font-size: 0.75rem; color: #d29922; flex-shrink: 0; display: none; }
235
+ .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; }
236
+ .fp-sync-bar .fp-btn { font-size: 0.7rem; padding: 0.25rem 0.5rem; }
237
+ .fp-sync-bar .sync-dir { font-size: 0.68rem; color: #8b949e; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
238
+ .fp-diff-item { display: flex; align-items: center; padding: 0.4rem 0.75rem; gap: 0.4rem; font-size: 0.78rem; color: #e6edf3; }
239
+ .fp-diff-item .badge { font-size: 0.65rem; padding: 0.1rem 0.35rem; border-radius: 4px; font-weight: 600; flex-shrink: 0; }
240
+ .badge-new { background: #238636; color: #fff; }
241
+ .badge-changed { background: #9e6a03; color: #fff; }
242
+ .badge-arrow { color: #8b949e; flex-shrink: 0; font-size: 0.7rem; }
243
+ .fp-diff-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
244
+ .fp-diff-summary { padding: 0.75rem; text-align: center; color: #8b949e; font-size: 0.8rem; }
245
+ .fp-diff-actions { display: flex; gap: 0.5rem; padding: 0.5rem 0.75rem; flex-shrink: 0; }
246
+ .fp-diff-actions .fp-btn { flex: 1; text-align: center; padding: 0.45rem; }
247
+ .fp-btn-primary { background: #238636; border-color: #2ea04380; }
248
+ .fp-btn-primary:active { background: #2ea043; }
249
+ .fp-btn-warn { background: #9e6a03; border-color: #d2992280; }
250
+ .fp-btn-warn:active { background: #bb8009; }
251
+
206
252
  /* ── 底部选择面板 ─────────────────────── */
207
253
  .action-sheet { display: none; position: fixed; inset: 0; z-index: 80; }
208
254
  .action-sheet.open { display: flex; align-items: flex-end; justify-content: center; }
@@ -243,7 +289,7 @@
243
289
  <div id="session-title">加载中...</div>
244
290
  <div id="session-cwd"></div>
245
291
  </div>
246
- <button class="top-btn" onclick="openFiles()" title="文件" style="font-size:0.7rem;color:var(--muted)">文件</button>
292
+ <button class="top-btn" onclick="openFilePanel()" title="文件" style="font-size:0.7rem;color:var(--muted)">文件</button>
247
293
  <button class="top-btn" onclick="showUsage()" title="用量" style="font-size:0.7rem;color:var(--muted)">用量</button>
248
294
  <button class="top-btn" onclick="openSettings()" title="设置">
249
295
  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 01-2.83 2.83l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
@@ -407,6 +453,31 @@
407
453
  </div>
408
454
  </div>
409
455
 
456
+ <!-- 文件管理面板 -->
457
+ <div id="file-panel-backdrop" onclick="closeFilePanel()"></div>
458
+ <div id="file-panel">
459
+ <div class="fp-header">
460
+ <h3 id="fp-title">文件管理</h3>
461
+ <button class="fp-btn" onclick="fpUploadClick()">上传</button>
462
+ <button class="fp-btn" onclick="fpStartSync()">同步</button>
463
+ <button class="fp-btn" onclick="closeFilePanel()">关闭</button>
464
+ </div>
465
+ <div class="fp-sync-bar" id="fp-sync-bar" style="display:none">
466
+ <span class="sync-dir" id="fp-sync-dir"></span>
467
+ <button class="fp-btn" onclick="fpPickDir()">换目录</button>
468
+ <button class="fp-btn" onclick="fpExitSync()">退出同步</button>
469
+ </div>
470
+ <div class="fp-path" id="fp-path"></div>
471
+ <div class="fp-list" id="fp-list"></div>
472
+ <div class="fp-diff-actions" id="fp-diff-actions" style="display:none">
473
+ <button class="fp-btn fp-btn-primary" onclick="fpSyncToLocal()">下载到本地</button>
474
+ <button class="fp-btn fp-btn-warn" onclick="fpSyncToServer()">上传到服务器</button>
475
+ </div>
476
+ <div class="fp-progress" id="fp-progress"></div>
477
+ <div class="fp-drop-zone" id="fp-drop">拖拽文件到此处上传,或点击上方按钮</div>
478
+ <input type="file" id="fp-file-input" multiple style="display:none">
479
+ </div>
480
+
410
481
  <div id="status-overlay">连接中...</div>
411
482
 
412
483
  <script>
@@ -1721,7 +1792,285 @@
1721
1792
  // ── 辅助 ──────────────────────────────────────
1722
1793
  function updateViewers(count) { document.getElementById('viewer-count').textContent = count > 1 ? `${count} 人在线` : ''; }
1723
1794
  window.goBack = function() { window.location.href = '/'; };
1724
- window.openFiles = function() { window.open('/files?session=' + SESSION_ID, '_blank'); };
1795
+ // ── 文件管理面板 ──────────────────────────────
1796
+ let fpCurrentPath = '';
1797
+ var fpQ = 'sessionId=' + encodeURIComponent(SESSION_ID);
1798
+ window.openFilePanel = function() {
1799
+ fpCurrentPath = '';
1800
+ fpLoadFiles();
1801
+ document.getElementById('file-panel').classList.add('open');
1802
+ document.getElementById('file-panel-backdrop').classList.add('open');
1803
+ };
1804
+ window.closeFilePanel = function() {
1805
+ document.getElementById('file-panel').classList.remove('open');
1806
+ document.getElementById('file-panel-backdrop').classList.remove('open');
1807
+ };
1808
+
1809
+ async function fpLoadFiles() {
1810
+ var list = document.getElementById('fp-list');
1811
+ list.innerHTML = '<div class="fp-empty">加载中...</div>';
1812
+ try {
1813
+ var r = await fetch('/api/files/list?path=' + encodeURIComponent(fpCurrentPath) + '&' + fpQ);
1814
+ var data = await r.json();
1815
+ if (data.error) { list.innerHTML = '<div class="fp-empty">' + escHtml(data.error) + '</div>'; return; }
1816
+ document.getElementById('fp-path').textContent = data.current || '';
1817
+ var html = '';
1818
+ if (data.parent) {
1819
+ html += '<div class="fp-item" onclick="fpNav(\'' + escHtml(data.parent).replace(/'/g, "\\'") + '\')"><span class="fp-icon">📁</span><span class="fp-name">..</span></div>';
1820
+ }
1821
+ for (var i = 0; i < data.entries.length; i++) {
1822
+ var e = data.entries[i];
1823
+ var full = data.current + '/' + e.name;
1824
+ if (e.type === 'dir') {
1825
+ html += '<div class="fp-item" onclick="fpNav(\'' + escHtml(full).replace(/'/g, "\\'") + '\')"><span class="fp-icon">📁</span><span class="fp-name">' + escHtml(e.name) + '</span></div>';
1826
+ } else {
1827
+ html += '<div class="fp-item"><span class="fp-icon">📄</span><span class="fp-name">' + escHtml(e.name) + '</span><span class="fp-size">' + fpFormatSize(e.size) + '</span><a class="fp-dl" href="/api/files/download?path=' + encodeURIComponent(full) + '&' + fpQ + '" download title="下载">下载</a></div>';
1828
+ }
1829
+ }
1830
+ list.innerHTML = html || '<div class="fp-empty">空目录</div>';
1831
+ } catch (e) {
1832
+ list.innerHTML = '<div class="fp-empty">加载失败: ' + escHtml(e.message) + '</div>';
1833
+ }
1834
+ }
1835
+
1836
+ window.fpNav = function(path) { fpCurrentPath = path; if (syncMode) fpComputeDiff(); else fpLoadFiles(); };
1837
+
1838
+ function fpFormatSize(bytes) {
1839
+ if (bytes == null) return '';
1840
+ if (bytes < 1024) return bytes + ' B';
1841
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
1842
+ if (bytes < 1024 * 1024 * 1024) return (bytes / 1024 / 1024).toFixed(1) + ' MB';
1843
+ return (bytes / 1024 / 1024 / 1024).toFixed(1) + ' GB';
1844
+ }
1845
+
1846
+ window.fpUploadClick = function() { document.getElementById('fp-file-input').click(); };
1847
+
1848
+ document.getElementById('fp-file-input').addEventListener('change', function(e) {
1849
+ var copy = Array.from(e.target.files);
1850
+ e.target.value = '';
1851
+ if (copy.length) fpUploadFiles(copy);
1852
+ });
1853
+
1854
+ async function fpUploadFiles(files) {
1855
+ if (!files.length) return;
1856
+ var progress = document.getElementById('fp-progress');
1857
+ progress.style.display = 'block';
1858
+ progress.textContent = '正在上传 ' + files.length + ' 个文件...';
1859
+ try {
1860
+ var form = new FormData();
1861
+ for (var i = 0; i < files.length; i++) form.append('files', files[i], files[i].webkitRelativePath || files[i].name);
1862
+ var r = await fetch('/api/files/upload?path=' + encodeURIComponent(fpCurrentPath) + '&' + fpQ, { method: 'POST', body: form });
1863
+ var data = await r.json();
1864
+ if (data.ok) {
1865
+ progress.textContent = '已上传 ' + data.files.length + ' 个文件';
1866
+ fpLoadFiles();
1867
+ } else {
1868
+ progress.textContent = '上传失败: ' + (data.error || '未知错误');
1869
+ }
1870
+ } catch (e) {
1871
+ progress.textContent = '上传出错: ' + e.message;
1872
+ }
1873
+ setTimeout(function() { progress.style.display = 'none'; }, 3000);
1874
+ }
1875
+
1876
+ // 拖拽上传
1877
+ var fpDropEl = document.getElementById('fp-drop');
1878
+ fpDropEl.addEventListener('dragover', function(e) { e.preventDefault(); fpDropEl.classList.add('dragover'); });
1879
+ fpDropEl.addEventListener('dragleave', function() { fpDropEl.classList.remove('dragover'); });
1880
+ fpDropEl.addEventListener('drop', function(e) {
1881
+ e.preventDefault();
1882
+ fpDropEl.classList.remove('dragover');
1883
+ if (e.dataTransfer.files.length) fpUploadFiles(Array.from(e.dataTransfer.files));
1884
+ });
1885
+
1886
+ // ── 文件同步(File System Access API)──────────
1887
+ var syncDirHandle = null;
1888
+ var syncMode = false;
1889
+ var syncDiff = { toDownload: [], toUpload: [] };
1890
+
1891
+ window.fpStartSync = async function() {
1892
+ if (!window.showDirectoryPicker) { alert('当前浏览器不支持文件夹访问,请使用 Chrome 或 Edge'); return; }
1893
+ if (!syncDirHandle) {
1894
+ await fpPickDirInner();
1895
+ if (!syncDirHandle) return;
1896
+ }
1897
+ syncMode = true;
1898
+ document.getElementById('fp-sync-bar').style.display = 'flex';
1899
+ document.getElementById('fp-drop').style.display = 'none';
1900
+ await fpComputeDiff();
1901
+ };
1902
+ window.fpPickDir = async function() { await fpPickDirInner(); };
1903
+ async function fpPickDirInner() {
1904
+ try {
1905
+ syncDirHandle = await window.showDirectoryPicker({ mode: 'readwrite' });
1906
+ document.getElementById('fp-sync-dir').textContent = syncDirHandle.name;
1907
+ if (syncMode) await fpComputeDiff();
1908
+ } catch (e) { if (e.name !== 'AbortError') alert('选择文件夹失败: ' + e.message); }
1909
+ }
1910
+ window.fpExitSync = function() {
1911
+ syncMode = false;
1912
+ document.getElementById('fp-sync-bar').style.display = 'none';
1913
+ document.getElementById('fp-diff-actions').style.display = 'none';
1914
+ document.getElementById('fp-drop').style.display = '';
1915
+ fpLoadFiles();
1916
+ };
1917
+
1918
+ async function walkLocalDir(dirHandle, prefix) {
1919
+ var files = [];
1920
+ for await (var entry of dirHandle.entries()) {
1921
+ var name = entry[0], handle = entry[1];
1922
+ if (name.startsWith('.')) continue;
1923
+ var rel = prefix ? prefix + '/' + name : name;
1924
+ if (handle.kind === 'directory') {
1925
+ files.push.apply(files, await walkLocalDir(handle, rel));
1926
+ } else {
1927
+ var file = await handle.getFile();
1928
+ files.push({ name: rel, size: file.size, handle: handle });
1929
+ }
1930
+ }
1931
+ return files;
1932
+ }
1933
+
1934
+ async function hashFile(file) {
1935
+ var buf = await file.arrayBuffer();
1936
+ var hash = await crypto.subtle.digest('SHA-256', buf);
1937
+ return Array.from(new Uint8Array(hash)).map(function(b) { return b.toString(16).padStart(2, '0'); }).join('');
1938
+ }
1939
+
1940
+ async function fpComputeDiff() {
1941
+ var list = document.getElementById('fp-list');
1942
+ list.innerHTML = '<div class="fp-empty">正在比对文件...</div>';
1943
+ var progress = document.getElementById('fp-progress');
1944
+ progress.style.display = 'block';
1945
+ try {
1946
+ progress.textContent = '获取服务器文件清单...';
1947
+ var r = await fetch('/api/files/list?path=' + encodeURIComponent(fpCurrentPath) + '&' + fpQ + '&recursive=1');
1948
+ var data = await r.json();
1949
+ if (data.error) throw new Error(data.error);
1950
+
1951
+ progress.textContent = '扫描本地文件...';
1952
+ var localFiles = await walkLocalDir(syncDirHandle, '');
1953
+ var serverFiles = (data.entries || []).filter(function(e) { return e.type === 'file'; });
1954
+ var serverMap = new Map(serverFiles.map(function(f) { return [f.name, f]; }));
1955
+ var localMap = new Map(localFiles.map(function(f) { return [f.name, f]; }));
1956
+
1957
+ var toDownload = [], toUpload = [];
1958
+ var checked = 0, total = serverMap.size + localMap.size;
1959
+
1960
+ for (var entry of serverMap) {
1961
+ var sName = entry[0], sf = entry[1];
1962
+ if (++checked % 20 === 0) progress.textContent = '比对中... ' + checked + '/' + total;
1963
+ var lf = localMap.get(sName);
1964
+ if (!lf) {
1965
+ toDownload.push({ name: sName, size: sf.size, reason: 'new' });
1966
+ } else if (lf.size !== sf.size) {
1967
+ toDownload.push({ name: sName, size: sf.size, reason: 'changed' });
1968
+ }
1969
+ }
1970
+ for (var entry2 of localMap) {
1971
+ var lName = entry2[0], lf2 = entry2[1];
1972
+ if (++checked % 20 === 0) progress.textContent = '比对中... ' + checked + '/' + total;
1973
+ var sf2 = serverMap.get(lName);
1974
+ if (!sf2) {
1975
+ toUpload.push({ name: lName, size: lf2.size, reason: 'new', handle: lf2.handle });
1976
+ } else if (lf2.size !== sf2.size) {
1977
+ toUpload.push({ name: lName, size: lf2.size, reason: 'changed', handle: lf2.handle });
1978
+ }
1979
+ }
1980
+
1981
+ syncDiff = { toDownload: toDownload, toUpload: toUpload };
1982
+ fpRenderDiff();
1983
+ } catch (e) {
1984
+ list.innerHTML = '<div class="fp-empty">比对失败: ' + escHtml(e.message) + '</div>';
1985
+ }
1986
+ progress.style.display = 'none';
1987
+ }
1988
+
1989
+ function fpRenderDiff() {
1990
+ var td = syncDiff.toDownload, tu = syncDiff.toUpload;
1991
+ var list = document.getElementById('fp-list');
1992
+ if (!td.length && !tu.length) {
1993
+ list.innerHTML = '<div class="fp-diff-summary">文件已同步,无差异</div>';
1994
+ document.getElementById('fp-diff-actions').style.display = 'none';
1995
+ return;
1996
+ }
1997
+ var html = '<div class="fp-diff-summary">';
1998
+ if (td.length) html += td.length + ' 个文件需下载到本地';
1999
+ if (td.length && tu.length) html += ',';
2000
+ if (tu.length) html += tu.length + ' 个文件需上传到服务器';
2001
+ html += '</div>';
2002
+ for (var i = 0; i < td.length; i++) {
2003
+ var f = td[i];
2004
+ html += '<div class="fp-diff-item"><span class="badge ' + (f.reason === 'new' ? 'badge-new' : 'badge-changed') + '">' + (f.reason === 'new' ? '新' : '改') + '</span><span class="badge-arrow">↓</span><span class="fp-diff-name">' + escHtml(f.name) + '</span><span class="fp-size">' + fpFormatSize(f.size) + '</span></div>';
2005
+ }
2006
+ for (var j = 0; j < tu.length; j++) {
2007
+ var g = tu[j];
2008
+ html += '<div class="fp-diff-item"><span class="badge ' + (g.reason === 'new' ? 'badge-new' : 'badge-changed') + '">' + (g.reason === 'new' ? '新' : '改') + '</span><span class="badge-arrow">↑</span><span class="fp-diff-name">' + escHtml(g.name) + '</span><span class="fp-size">' + fpFormatSize(g.size) + '</span></div>';
2009
+ }
2010
+ list.innerHTML = html;
2011
+ document.getElementById('fp-diff-actions').style.display = 'flex';
2012
+ }
2013
+
2014
+ window.fpSyncToLocal = async function() {
2015
+ var toDownload = syncDiff.toDownload;
2016
+ if (!toDownload.length) return;
2017
+ var progress = document.getElementById('fp-progress');
2018
+ progress.style.display = 'block';
2019
+ var done = 0;
2020
+ for (var i = 0; i < toDownload.length; i++) {
2021
+ var f = toDownload[i];
2022
+ progress.textContent = '下载中 ' + (++done) + '/' + toDownload.length + ': ' + f.name;
2023
+ try {
2024
+ var relPath = fpCurrentPath ? fpCurrentPath + '/' + f.name : f.name;
2025
+ var r = await fetch('/api/files/download?path=' + encodeURIComponent(relPath) + '&' + fpQ);
2026
+ if (!r.ok) continue;
2027
+ var blob = await r.blob();
2028
+ var parts = f.name.split('/');
2029
+ var dir = syncDirHandle;
2030
+ for (var p = 0; p < parts.length - 1; p++) {
2031
+ dir = await dir.getDirectoryHandle(parts[p], { create: true });
2032
+ }
2033
+ var fh = await dir.getFileHandle(parts[parts.length - 1], { create: true });
2034
+ var w = await fh.createWritable();
2035
+ await w.write(blob);
2036
+ await w.close();
2037
+ } catch (e) { console.error('同步下载失败 ' + f.name + ':', e); }
2038
+ }
2039
+ progress.textContent = '已下载 ' + done + ' 个文件到本地';
2040
+ setTimeout(function() { progress.style.display = 'none'; }, 3000);
2041
+ await fpComputeDiff();
2042
+ };
2043
+
2044
+ window.fpSyncToServer = async function() {
2045
+ var toUpload = syncDiff.toUpload;
2046
+ if (!toUpload.length) return;
2047
+ var progress = document.getElementById('fp-progress');
2048
+ progress.style.display = 'block';
2049
+ var BATCH = 20, done = 0;
2050
+ for (var i = 0; i < toUpload.length; i += BATCH) {
2051
+ var batch = toUpload.slice(i, i + BATCH);
2052
+ progress.textContent = '上传中 ' + done + '/' + toUpload.length + '...';
2053
+ var form = new FormData();
2054
+ for (var b = 0; b < batch.length; b++) {
2055
+ try { form.append('files', await batch[b].handle.getFile(), batch[b].name); }
2056
+ catch (e) { console.error('读取失败 ' + batch[b].name + ':', e); }
2057
+ }
2058
+ try {
2059
+ await fetch('/api/files/upload?path=' + encodeURIComponent(fpCurrentPath) + '&' + fpQ, { method: 'POST', body: form });
2060
+ } catch (e) { console.error('上传批次失败:', e); }
2061
+ done += batch.length;
2062
+ }
2063
+ progress.textContent = '已上传 ' + done + ' 个文件到服务器';
2064
+ setTimeout(function() { progress.style.display = 'none'; }, 3000);
2065
+ await fpComputeDiff();
2066
+ };
2067
+
2068
+ // 监听文件变更
2069
+ socket.on('files:changed', function(data) {
2070
+ if (data.sessionId === SESSION_ID && document.getElementById('file-panel').classList.contains('open')) {
2071
+ if (syncMode) fpComputeDiff(); else fpLoadFiles();
2072
+ }
2073
+ });
1725
2074
  function scrollToBottom() { requestAnimationFrame(() => { chatArea.scrollTop = chatArea.scrollHeight; }); }
1726
2075
  function trimMessages() {
1727
2076
  const msgs = chatArea.querySelectorAll('.msg');