@wendongfly/zihi 1.1.5 → 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.
Files changed (3) hide show
  1. package/dist/chat.html +209 -24
  2. package/dist/index.js +108 -108
  3. package/package.json +1 -1
package/dist/chat.html CHANGED
@@ -235,6 +235,19 @@
235
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
236
  .fp-sync-bar .fp-btn { font-size: 0.7rem; padding: 0.25rem 0.5rem; }
237
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; }
238
251
 
239
252
  /* ── 底部选择面板 ─────────────────────── */
240
253
  .action-sheet { display: none; position: fixed; inset: 0; z-index: 80; }
@@ -456,6 +469,10 @@
456
469
  </div>
457
470
  <div class="fp-path" id="fp-path"></div>
458
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>
459
476
  <div class="fp-progress" id="fp-progress"></div>
460
477
  <div class="fp-drop-zone" id="fp-drop">拖拽文件到此处上传,或点击上方按钮</div>
461
478
  <input type="file" id="fp-file-input" multiple style="display:none">
@@ -1777,6 +1794,7 @@
1777
1794
  window.goBack = function() { window.location.href = '/'; };
1778
1795
  // ── 文件管理面板 ──────────────────────────────
1779
1796
  let fpCurrentPath = '';
1797
+ var fpQ = 'sessionId=' + encodeURIComponent(SESSION_ID);
1780
1798
  window.openFilePanel = function() {
1781
1799
  fpCurrentPath = '';
1782
1800
  fpLoadFiles();
@@ -1789,23 +1807,24 @@
1789
1807
  };
1790
1808
 
1791
1809
  async function fpLoadFiles() {
1792
- const list = document.getElementById('fp-list');
1810
+ var list = document.getElementById('fp-list');
1793
1811
  list.innerHTML = '<div class="fp-empty">加载中...</div>';
1794
1812
  try {
1795
- const r = await fetch('/api/files/list?path=' + encodeURIComponent(fpCurrentPath));
1796
- const data = await r.json();
1813
+ var r = await fetch('/api/files/list?path=' + encodeURIComponent(fpCurrentPath) + '&' + fpQ);
1814
+ var data = await r.json();
1797
1815
  if (data.error) { list.innerHTML = '<div class="fp-empty">' + escHtml(data.error) + '</div>'; return; }
1798
1816
  document.getElementById('fp-path').textContent = data.current || '';
1799
- let html = '';
1817
+ var html = '';
1800
1818
  if (data.parent) {
1801
1819
  html += '<div class="fp-item" onclick="fpNav(\'' + escHtml(data.parent).replace(/'/g, "\\'") + '\')"><span class="fp-icon">📁</span><span class="fp-name">..</span></div>';
1802
1820
  }
1803
- for (const e of data.entries) {
1804
- const full = data.current + '/' + e.name;
1821
+ for (var i = 0; i < data.entries.length; i++) {
1822
+ var e = data.entries[i];
1823
+ var full = data.current + '/' + e.name;
1805
1824
  if (e.type === 'dir') {
1806
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>';
1807
1826
  } else {
1808
- 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) + '" download title="下载">下载</a></div>';
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>';
1809
1828
  }
1810
1829
  }
1811
1830
  list.innerHTML = html || '<div class="fp-empty">空目录</div>';
@@ -1814,7 +1833,7 @@
1814
1833
  }
1815
1834
  }
1816
1835
 
1817
- window.fpNav = function(path) { fpCurrentPath = path; fpLoadFiles(); };
1836
+ window.fpNav = function(path) { fpCurrentPath = path; if (syncMode) fpComputeDiff(); else fpLoadFiles(); };
1818
1837
 
1819
1838
  function fpFormatSize(bytes) {
1820
1839
  if (bytes == null) return '';
@@ -1827,20 +1846,21 @@
1827
1846
  window.fpUploadClick = function() { document.getElementById('fp-file-input').click(); };
1828
1847
 
1829
1848
  document.getElementById('fp-file-input').addEventListener('change', function(e) {
1830
- if (e.target.files.length) fpUploadFiles(e.target.files);
1849
+ var copy = Array.from(e.target.files);
1831
1850
  e.target.value = '';
1851
+ if (copy.length) fpUploadFiles(copy);
1832
1852
  });
1833
1853
 
1834
1854
  async function fpUploadFiles(files) {
1835
1855
  if (!files.length) return;
1836
- const progress = document.getElementById('fp-progress');
1856
+ var progress = document.getElementById('fp-progress');
1837
1857
  progress.style.display = 'block';
1838
1858
  progress.textContent = '正在上传 ' + files.length + ' 个文件...';
1839
1859
  try {
1840
- const form = new FormData();
1841
- for (const file of files) form.append('files', file, file.webkitRelativePath || file.name);
1842
- const r = await fetch('/api/files/upload?path=' + encodeURIComponent(fpCurrentPath), { method: 'POST', body: form });
1843
- const data = await r.json();
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();
1844
1864
  if (data.ok) {
1845
1865
  progress.textContent = '已上传 ' + data.files.length + ' 个文件';
1846
1866
  fpLoadFiles();
@@ -1860,31 +1880,196 @@
1860
1880
  fpDropEl.addEventListener('drop', function(e) {
1861
1881
  e.preventDefault();
1862
1882
  fpDropEl.classList.remove('dragover');
1863
- if (e.dataTransfer.files.length) fpUploadFiles(e.dataTransfer.files);
1883
+ if (e.dataTransfer.files.length) fpUploadFiles(Array.from(e.dataTransfer.files));
1864
1884
  });
1865
1885
 
1866
- // 文件同步(File System Access API
1867
- let syncDirHandle = null;
1886
+ // ── 文件同步(File System Access API)──────────
1887
+ var syncDirHandle = null;
1888
+ var syncMode = false;
1889
+ var syncDiff = { toDownload: [], toUpload: [] };
1890
+
1868
1891
  window.fpStartSync = async function() {
1869
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() {
1870
1904
  try {
1871
1905
  syncDirHandle = await window.showDirectoryPicker({ mode: 'readwrite' });
1872
- document.getElementById('fp-sync-bar').style.display = 'flex';
1873
1906
  document.getElementById('fp-sync-dir').textContent = syncDirHandle.name;
1874
- document.getElementById('fp-drop').style.display = 'none';
1907
+ if (syncMode) await fpComputeDiff();
1875
1908
  } catch (e) { if (e.name !== 'AbortError') alert('选择文件夹失败: ' + e.message); }
1876
- };
1877
- window.fpPickDir = async function() { await window.fpStartSync(); };
1909
+ }
1878
1910
  window.fpExitSync = function() {
1879
- syncDirHandle = null;
1911
+ syncMode = false;
1880
1912
  document.getElementById('fp-sync-bar').style.display = 'none';
1913
+ document.getElementById('fp-diff-actions').style.display = 'none';
1881
1914
  document.getElementById('fp-drop').style.display = '';
1882
1915
  fpLoadFiles();
1883
1916
  };
1884
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
+
1885
2068
  // 监听文件变更
1886
- socket.on('files:changed', function() {
1887
- if (document.getElementById('file-panel').classList.contains('open')) fpLoadFiles();
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
+ }
1888
2073
  });
1889
2074
  function scrollToBottom() { requestAnimationFrame(() => { chatArea.scrollTop = chatArea.scrollHeight; }); }
1890
2075
  function trimMessages() {