@wendongfly/zihi 1.1.5 → 1.1.7

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
@@ -224,6 +224,7 @@
224
224
  .fp-item .fp-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
225
225
  .fp-item .fp-size { color: #8b949e; font-size: 0.7rem; white-space: nowrap; }
226
226
  .fp-item .fp-dl { color: #58a6ff; font-size: 0.7rem; text-decoration: none; padding: 0.2rem 0.4rem; }
227
+ .fp-item .fp-del { color: #f85149; font-size: 0.7rem; cursor: pointer; padding: 0.2rem 0.4rem; background: none; border: none; }
227
228
  .fp-empty { text-align: center; color: #8b949e; padding: 2rem 0.75rem; font-size: 0.85rem; }
228
229
  .fp-drop-zone {
229
230
  margin: 0.5rem 0.75rem; padding: 1.5rem; border: 2px dashed #30363d; border-radius: 8px;
@@ -235,6 +236,19 @@
235
236
  .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
237
  .fp-sync-bar .fp-btn { font-size: 0.7rem; padding: 0.25rem 0.5rem; }
237
238
  .fp-sync-bar .sync-dir { font-size: 0.68rem; color: #8b949e; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
239
+ .fp-diff-item { display: flex; align-items: center; padding: 0.4rem 0.75rem; gap: 0.4rem; font-size: 0.78rem; color: #e6edf3; }
240
+ .fp-diff-item .badge { font-size: 0.65rem; padding: 0.1rem 0.35rem; border-radius: 4px; font-weight: 600; flex-shrink: 0; }
241
+ .badge-new { background: #238636; color: #fff; }
242
+ .badge-changed { background: #9e6a03; color: #fff; }
243
+ .badge-arrow { color: #8b949e; flex-shrink: 0; font-size: 0.7rem; }
244
+ .fp-diff-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
245
+ .fp-diff-summary { padding: 0.75rem; text-align: center; color: #8b949e; font-size: 0.8rem; }
246
+ .fp-diff-actions { display: flex; gap: 0.5rem; padding: 0.5rem 0.75rem; flex-shrink: 0; }
247
+ .fp-diff-actions .fp-btn { flex: 1; text-align: center; padding: 0.45rem; }
248
+ .fp-btn-primary { background: #238636; border-color: #2ea04380; }
249
+ .fp-btn-primary:active { background: #2ea043; }
250
+ .fp-btn-warn { background: #9e6a03; border-color: #d2992280; }
251
+ .fp-btn-warn:active { background: #bb8009; }
238
252
 
239
253
  /* ── 底部选择面板 ─────────────────────── */
240
254
  .action-sheet { display: none; position: fixed; inset: 0; z-index: 80; }
@@ -446,16 +460,20 @@
446
460
  <div class="fp-header">
447
461
  <h3 id="fp-title">文件管理</h3>
448
462
  <button class="fp-btn" onclick="fpUploadClick()">上传</button>
449
- <button class="fp-btn" onclick="fpStartSync()">同步</button>
463
+ <button class="fp-btn" onclick="fpMkdir()">新建</button>
450
464
  <button class="fp-btn" onclick="closeFilePanel()">关闭</button>
451
465
  </div>
452
466
  <div class="fp-sync-bar" id="fp-sync-bar" style="display:none">
453
467
  <span class="sync-dir" id="fp-sync-dir"></span>
454
468
  <button class="fp-btn" onclick="fpPickDir()">换目录</button>
455
- <button class="fp-btn" onclick="fpExitSync()">退出同步</button>
469
+ <button class="fp-btn" onclick="fpExitSync()">关闭同步</button>
456
470
  </div>
457
471
  <div class="fp-path" id="fp-path"></div>
458
472
  <div class="fp-list" id="fp-list"></div>
473
+ <div class="fp-diff-actions" id="fp-diff-actions" style="display:none">
474
+ <button class="fp-btn fp-btn-primary" onclick="fpSyncToLocal()">下载到本地</button>
475
+ <button class="fp-btn fp-btn-warn" onclick="fpSyncToServer()">上传到服务器</button>
476
+ </div>
459
477
  <div class="fp-progress" id="fp-progress"></div>
460
478
  <div class="fp-drop-zone" id="fp-drop">拖拽文件到此处上传,或点击上方按钮</div>
461
479
  <input type="file" id="fp-file-input" multiple style="display:none">
@@ -1777,6 +1795,7 @@
1777
1795
  window.goBack = function() { window.location.href = '/'; };
1778
1796
  // ── 文件管理面板 ──────────────────────────────
1779
1797
  let fpCurrentPath = '';
1798
+ var fpQ = 'sessionId=' + encodeURIComponent(SESSION_ID);
1780
1799
  window.openFilePanel = function() {
1781
1800
  fpCurrentPath = '';
1782
1801
  fpLoadFiles();
@@ -1789,23 +1808,25 @@
1789
1808
  };
1790
1809
 
1791
1810
  async function fpLoadFiles() {
1792
- const list = document.getElementById('fp-list');
1811
+ var list = document.getElementById('fp-list');
1793
1812
  list.innerHTML = '<div class="fp-empty">加载中...</div>';
1794
1813
  try {
1795
- const r = await fetch('/api/files/list?path=' + encodeURIComponent(fpCurrentPath));
1796
- const data = await r.json();
1814
+ var r = await fetch('/api/files/list?path=' + encodeURIComponent(fpCurrentPath) + '&' + fpQ);
1815
+ var data = await r.json();
1797
1816
  if (data.error) { list.innerHTML = '<div class="fp-empty">' + escHtml(data.error) + '</div>'; return; }
1798
1817
  document.getElementById('fp-path').textContent = data.current || '';
1799
- let html = '';
1818
+ var html = '';
1800
1819
  if (data.parent) {
1801
1820
  html += '<div class="fp-item" onclick="fpNav(\'' + escHtml(data.parent).replace(/'/g, "\\'") + '\')"><span class="fp-icon">📁</span><span class="fp-name">..</span></div>';
1802
1821
  }
1803
- for (const e of data.entries) {
1804
- const full = data.current + '/' + e.name;
1822
+ for (var i = 0; i < data.entries.length; i++) {
1823
+ var e = data.entries[i];
1824
+ var full = data.current + '/' + e.name;
1825
+ var eFull = escHtml(full).replace(/'/g, "\\'");
1805
1826
  if (e.type === 'dir') {
1806
- 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>';
1827
+ html += '<div class="fp-item"><span class="fp-icon" onclick="fpNav(\'' + eFull + '\')">📁</span><span class="fp-name" onclick="fpNav(\'' + eFull + '\')">' + escHtml(e.name) + '</span><button class="fp-del" onclick="event.stopPropagation();fpDelete(\'' + eFull + '\')">删除</button></div>';
1807
1828
  } 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>';
1829
+ 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><button class="fp-del" onclick="event.stopPropagation();fpDelete(\'' + eFull + '\')">删除</button></div>';
1809
1830
  }
1810
1831
  }
1811
1832
  list.innerHTML = html || '<div class="fp-empty">空目录</div>';
@@ -1814,7 +1835,37 @@
1814
1835
  }
1815
1836
  }
1816
1837
 
1817
- window.fpNav = function(path) { fpCurrentPath = path; fpLoadFiles(); };
1838
+ window.fpNav = function(path) { fpCurrentPath = path; if (syncMode) fpComputeDiff(); else fpLoadFiles(); };
1839
+
1840
+ window.fpMkdir = async function() {
1841
+ var name = prompt('文件夹名称:');
1842
+ if (!name) return;
1843
+ var dirPath = fpCurrentPath ? fpCurrentPath + '/' + name : name;
1844
+ try {
1845
+ var r = await fetch('/api/files/mkdir', {
1846
+ method: 'POST',
1847
+ headers: { 'Content-Type': 'application/json' },
1848
+ body: JSON.stringify({ path: dirPath, sessionId: SESSION_ID }),
1849
+ });
1850
+ var data = await r.json();
1851
+ if (data.ok) fpLoadFiles();
1852
+ else alert('创建失败: ' + (data.error || '未知错误'));
1853
+ } catch (e) { alert('创建失败: ' + e.message); }
1854
+ };
1855
+
1856
+ window.fpDelete = async function(path) {
1857
+ if (!confirm('确定删除?')) return;
1858
+ try {
1859
+ var r = await fetch('/api/files/delete', {
1860
+ method: 'DELETE',
1861
+ headers: { 'Content-Type': 'application/json' },
1862
+ body: JSON.stringify({ path: path, sessionId: SESSION_ID }),
1863
+ });
1864
+ var data = await r.json();
1865
+ if (data.ok) fpLoadFiles();
1866
+ else alert('删除失败: ' + (data.error || '未知错误'));
1867
+ } catch (e) { alert('删除失败: ' + e.message); }
1868
+ };
1818
1869
 
1819
1870
  function fpFormatSize(bytes) {
1820
1871
  if (bytes == null) return '';
@@ -1827,20 +1878,21 @@
1827
1878
  window.fpUploadClick = function() { document.getElementById('fp-file-input').click(); };
1828
1879
 
1829
1880
  document.getElementById('fp-file-input').addEventListener('change', function(e) {
1830
- if (e.target.files.length) fpUploadFiles(e.target.files);
1881
+ var copy = Array.from(e.target.files);
1831
1882
  e.target.value = '';
1883
+ if (copy.length) fpUploadFiles(copy);
1832
1884
  });
1833
1885
 
1834
1886
  async function fpUploadFiles(files) {
1835
1887
  if (!files.length) return;
1836
- const progress = document.getElementById('fp-progress');
1888
+ var progress = document.getElementById('fp-progress');
1837
1889
  progress.style.display = 'block';
1838
1890
  progress.textContent = '正在上传 ' + files.length + ' 个文件...';
1839
1891
  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();
1892
+ var form = new FormData();
1893
+ for (var i = 0; i < files.length; i++) form.append('files', files[i], files[i].webkitRelativePath || files[i].name);
1894
+ var r = await fetch('/api/files/upload?path=' + encodeURIComponent(fpCurrentPath) + '&' + fpQ, { method: 'POST', body: form });
1895
+ var data = await r.json();
1844
1896
  if (data.ok) {
1845
1897
  progress.textContent = '已上传 ' + data.files.length + ' 个文件';
1846
1898
  fpLoadFiles();
@@ -1860,31 +1912,196 @@
1860
1912
  fpDropEl.addEventListener('drop', function(e) {
1861
1913
  e.preventDefault();
1862
1914
  fpDropEl.classList.remove('dragover');
1863
- if (e.dataTransfer.files.length) fpUploadFiles(e.dataTransfer.files);
1915
+ if (e.dataTransfer.files.length) fpUploadFiles(Array.from(e.dataTransfer.files));
1864
1916
  });
1865
1917
 
1866
- // 文件同步(File System Access API
1867
- let syncDirHandle = null;
1918
+ // ── 文件同步(File System Access API)──────────
1919
+ var syncDirHandle = null;
1920
+ var syncMode = false;
1921
+ var syncDiff = { toDownload: [], toUpload: [] };
1922
+
1868
1923
  window.fpStartSync = async function() {
1869
1924
  if (!window.showDirectoryPicker) { alert('当前浏览器不支持文件夹访问,请使用 Chrome 或 Edge'); return; }
1925
+ if (!syncDirHandle) {
1926
+ await fpPickDirInner();
1927
+ if (!syncDirHandle) return;
1928
+ }
1929
+ syncMode = true;
1930
+ document.getElementById('fp-sync-bar').style.display = 'flex';
1931
+ document.getElementById('fp-drop').style.display = 'none';
1932
+ await fpComputeDiff();
1933
+ };
1934
+ window.fpPickDir = async function() { await fpPickDirInner(); };
1935
+ async function fpPickDirInner() {
1870
1936
  try {
1871
1937
  syncDirHandle = await window.showDirectoryPicker({ mode: 'readwrite' });
1872
- document.getElementById('fp-sync-bar').style.display = 'flex';
1873
1938
  document.getElementById('fp-sync-dir').textContent = syncDirHandle.name;
1874
- document.getElementById('fp-drop').style.display = 'none';
1939
+ if (syncMode) await fpComputeDiff();
1875
1940
  } catch (e) { if (e.name !== 'AbortError') alert('选择文件夹失败: ' + e.message); }
1876
- };
1877
- window.fpPickDir = async function() { await window.fpStartSync(); };
1941
+ }
1878
1942
  window.fpExitSync = function() {
1879
- syncDirHandle = null;
1943
+ syncMode = false;
1880
1944
  document.getElementById('fp-sync-bar').style.display = 'none';
1945
+ document.getElementById('fp-diff-actions').style.display = 'none';
1881
1946
  document.getElementById('fp-drop').style.display = '';
1882
1947
  fpLoadFiles();
1883
1948
  };
1884
1949
 
1950
+ async function walkLocalDir(dirHandle, prefix) {
1951
+ var files = [];
1952
+ for await (var entry of dirHandle.entries()) {
1953
+ var name = entry[0], handle = entry[1];
1954
+ if (name.startsWith('.')) continue;
1955
+ var rel = prefix ? prefix + '/' + name : name;
1956
+ if (handle.kind === 'directory') {
1957
+ files.push.apply(files, await walkLocalDir(handle, rel));
1958
+ } else {
1959
+ var file = await handle.getFile();
1960
+ files.push({ name: rel, size: file.size, handle: handle });
1961
+ }
1962
+ }
1963
+ return files;
1964
+ }
1965
+
1966
+ async function hashFile(file) {
1967
+ var buf = await file.arrayBuffer();
1968
+ var hash = await crypto.subtle.digest('SHA-256', buf);
1969
+ return Array.from(new Uint8Array(hash)).map(function(b) { return b.toString(16).padStart(2, '0'); }).join('');
1970
+ }
1971
+
1972
+ async function fpComputeDiff() {
1973
+ var list = document.getElementById('fp-list');
1974
+ list.innerHTML = '<div class="fp-empty">正在比对文件...</div>';
1975
+ var progress = document.getElementById('fp-progress');
1976
+ progress.style.display = 'block';
1977
+ try {
1978
+ progress.textContent = '获取服务器文件清单...';
1979
+ var r = await fetch('/api/files/list?path=' + encodeURIComponent(fpCurrentPath) + '&' + fpQ + '&recursive=1');
1980
+ var data = await r.json();
1981
+ if (data.error) throw new Error(data.error);
1982
+
1983
+ progress.textContent = '扫描本地文件...';
1984
+ var localFiles = await walkLocalDir(syncDirHandle, '');
1985
+ var serverFiles = (data.entries || []).filter(function(e) { return e.type === 'file'; });
1986
+ var serverMap = new Map(serverFiles.map(function(f) { return [f.name, f]; }));
1987
+ var localMap = new Map(localFiles.map(function(f) { return [f.name, f]; }));
1988
+
1989
+ var toDownload = [], toUpload = [];
1990
+ var checked = 0, total = serverMap.size + localMap.size;
1991
+
1992
+ for (var entry of serverMap) {
1993
+ var sName = entry[0], sf = entry[1];
1994
+ if (++checked % 20 === 0) progress.textContent = '比对中... ' + checked + '/' + total;
1995
+ var lf = localMap.get(sName);
1996
+ if (!lf) {
1997
+ toDownload.push({ name: sName, size: sf.size, reason: 'new' });
1998
+ } else if (lf.size !== sf.size) {
1999
+ toDownload.push({ name: sName, size: sf.size, reason: 'changed' });
2000
+ }
2001
+ }
2002
+ for (var entry2 of localMap) {
2003
+ var lName = entry2[0], lf2 = entry2[1];
2004
+ if (++checked % 20 === 0) progress.textContent = '比对中... ' + checked + '/' + total;
2005
+ var sf2 = serverMap.get(lName);
2006
+ if (!sf2) {
2007
+ toUpload.push({ name: lName, size: lf2.size, reason: 'new', handle: lf2.handle });
2008
+ } else if (lf2.size !== sf2.size) {
2009
+ toUpload.push({ name: lName, size: lf2.size, reason: 'changed', handle: lf2.handle });
2010
+ }
2011
+ }
2012
+
2013
+ syncDiff = { toDownload: toDownload, toUpload: toUpload };
2014
+ fpRenderDiff();
2015
+ } catch (e) {
2016
+ list.innerHTML = '<div class="fp-empty">比对失败: ' + escHtml(e.message) + '</div>';
2017
+ }
2018
+ progress.style.display = 'none';
2019
+ }
2020
+
2021
+ function fpRenderDiff() {
2022
+ var td = syncDiff.toDownload, tu = syncDiff.toUpload;
2023
+ var list = document.getElementById('fp-list');
2024
+ if (!td.length && !tu.length) {
2025
+ list.innerHTML = '<div class="fp-diff-summary">文件已同步,无差异</div>';
2026
+ document.getElementById('fp-diff-actions').style.display = 'none';
2027
+ return;
2028
+ }
2029
+ var html = '<div class="fp-diff-summary">';
2030
+ if (td.length) html += td.length + ' 个文件需下载到本地';
2031
+ if (td.length && tu.length) html += ',';
2032
+ if (tu.length) html += tu.length + ' 个文件需上传到服务器';
2033
+ html += '</div>';
2034
+ for (var i = 0; i < td.length; i++) {
2035
+ var f = td[i];
2036
+ 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>';
2037
+ }
2038
+ for (var j = 0; j < tu.length; j++) {
2039
+ var g = tu[j];
2040
+ 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>';
2041
+ }
2042
+ list.innerHTML = html;
2043
+ document.getElementById('fp-diff-actions').style.display = 'flex';
2044
+ }
2045
+
2046
+ window.fpSyncToLocal = async function() {
2047
+ var toDownload = syncDiff.toDownload;
2048
+ if (!toDownload.length) return;
2049
+ var progress = document.getElementById('fp-progress');
2050
+ progress.style.display = 'block';
2051
+ var done = 0;
2052
+ for (var i = 0; i < toDownload.length; i++) {
2053
+ var f = toDownload[i];
2054
+ progress.textContent = '下载中 ' + (++done) + '/' + toDownload.length + ': ' + f.name;
2055
+ try {
2056
+ var relPath = fpCurrentPath ? fpCurrentPath + '/' + f.name : f.name;
2057
+ var r = await fetch('/api/files/download?path=' + encodeURIComponent(relPath) + '&' + fpQ);
2058
+ if (!r.ok) continue;
2059
+ var blob = await r.blob();
2060
+ var parts = f.name.split('/');
2061
+ var dir = syncDirHandle;
2062
+ for (var p = 0; p < parts.length - 1; p++) {
2063
+ dir = await dir.getDirectoryHandle(parts[p], { create: true });
2064
+ }
2065
+ var fh = await dir.getFileHandle(parts[parts.length - 1], { create: true });
2066
+ var w = await fh.createWritable();
2067
+ await w.write(blob);
2068
+ await w.close();
2069
+ } catch (e) { console.error('同步下载失败 ' + f.name + ':', e); }
2070
+ }
2071
+ progress.textContent = '已下载 ' + done + ' 个文件到本地';
2072
+ setTimeout(function() { progress.style.display = 'none'; }, 3000);
2073
+ await fpComputeDiff();
2074
+ };
2075
+
2076
+ window.fpSyncToServer = async function() {
2077
+ var toUpload = syncDiff.toUpload;
2078
+ if (!toUpload.length) return;
2079
+ var progress = document.getElementById('fp-progress');
2080
+ progress.style.display = 'block';
2081
+ var BATCH = 20, done = 0;
2082
+ for (var i = 0; i < toUpload.length; i += BATCH) {
2083
+ var batch = toUpload.slice(i, i + BATCH);
2084
+ progress.textContent = '上传中 ' + done + '/' + toUpload.length + '...';
2085
+ var form = new FormData();
2086
+ for (var b = 0; b < batch.length; b++) {
2087
+ try { form.append('files', await batch[b].handle.getFile(), batch[b].name); }
2088
+ catch (e) { console.error('读取失败 ' + batch[b].name + ':', e); }
2089
+ }
2090
+ try {
2091
+ await fetch('/api/files/upload?path=' + encodeURIComponent(fpCurrentPath) + '&' + fpQ, { method: 'POST', body: form });
2092
+ } catch (e) { console.error('上传批次失败:', e); }
2093
+ done += batch.length;
2094
+ }
2095
+ progress.textContent = '已上传 ' + done + ' 个文件到服务器';
2096
+ setTimeout(function() { progress.style.display = 'none'; }, 3000);
2097
+ await fpComputeDiff();
2098
+ };
2099
+
1885
2100
  // 监听文件变更
1886
- socket.on('files:changed', function() {
1887
- if (document.getElementById('file-panel').classList.contains('open')) fpLoadFiles();
2101
+ socket.on('files:changed', function(data) {
2102
+ if (data.sessionId === SESSION_ID && document.getElementById('file-panel').classList.contains('open')) {
2103
+ if (syncMode) fpComputeDiff(); else fpLoadFiles();
2104
+ }
1888
2105
  });
1889
2106
  function scrollToBottom() { requestAnimationFrame(() => { chatArea.scrollTop = chatArea.scrollHeight; }); }
1890
2107
  function trimMessages() {