@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.
- package/dist/chat.html +209 -24
- package/dist/index.js +108 -108
- 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
|
-
|
|
1810
|
+
var list = document.getElementById('fp-list');
|
|
1793
1811
|
list.innerHTML = '<div class="fp-empty">加载中...</div>';
|
|
1794
1812
|
try {
|
|
1795
|
-
|
|
1796
|
-
|
|
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
|
-
|
|
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 (
|
|
1804
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1856
|
+
var progress = document.getElementById('fp-progress');
|
|
1837
1857
|
progress.style.display = 'block';
|
|
1838
1858
|
progress.textContent = '正在上传 ' + files.length + ' 个文件...';
|
|
1839
1859
|
try {
|
|
1840
|
-
|
|
1841
|
-
for (
|
|
1842
|
-
|
|
1843
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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'))
|
|
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() {
|