@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/chat.html CHANGED
@@ -172,6 +172,52 @@
172
172
  .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; }
173
173
  .setting-toggle input[type=checkbox]:checked::after { left: 18px; }
174
174
 
175
+ /* ── 文件管理面板 ─────────────────────── */
176
+ #file-panel-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 62; display: none; }
177
+ #file-panel-backdrop.open { display: block; }
178
+ #file-panel {
179
+ position: fixed; top: 0; right: -340px; width: 320px; max-width: 90vw; height: 100%;
180
+ background: #161b22; border-left: 1px solid #30363d; z-index: 63;
181
+ transition: right 0.25s ease; display: flex; flex-direction: column;
182
+ }
183
+ #file-panel.open { right: 0; }
184
+ .fp-header { display: flex; align-items: center; padding: 0.75rem; border-bottom: 1px solid #30363d; gap: 0.5rem; flex-shrink: 0; }
185
+ .fp-header h3 { flex: 1; font-size: 0.9rem; margin: 0; color: #e6edf3; }
186
+ .fp-btn { background: #21262d; color: #e6edf3; border: 1px solid #30363d; border-radius: 6px; padding: 0.3rem 0.6rem; font-size: 0.75rem; cursor: pointer; }
187
+ .fp-btn:active { background: #30363d; }
188
+ .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; }
189
+ .fp-list { flex: 1; overflow-y: auto; padding: 0.25rem 0; }
190
+ .fp-item { display: flex; align-items: center; padding: 0.5rem 0.75rem; gap: 0.5rem; cursor: pointer; font-size: 0.82rem; color: #e6edf3; }
191
+ .fp-item:active { background: #21262d; }
192
+ .fp-item .fp-icon { width: 1.2rem; text-align: center; flex-shrink: 0; }
193
+ .fp-item .fp-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
194
+ .fp-item .fp-size { color: #8b949e; font-size: 0.7rem; white-space: nowrap; }
195
+ .fp-item .fp-dl { color: #58a6ff; font-size: 0.7rem; text-decoration: none; padding: 0.2rem 0.4rem; }
196
+ .fp-empty { text-align: center; color: #8b949e; padding: 2rem 0.75rem; font-size: 0.85rem; }
197
+ .fp-drop-zone {
198
+ margin: 0.5rem 0.75rem; padding: 1.5rem; border: 2px dashed #30363d; border-radius: 8px;
199
+ text-align: center; color: #8b949e; font-size: 0.8rem; flex-shrink: 0;
200
+ transition: border-color 0.2s, background 0.2s;
201
+ }
202
+ .fp-drop-zone.dragover { border-color: #58a6ff; background: rgba(88,166,255,0.08); }
203
+ .fp-progress { padding: 0.4rem 0.75rem; font-size: 0.75rem; color: #d29922; flex-shrink: 0; display: none; }
204
+ .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; }
205
+ .fp-sync-bar .fp-btn { font-size: 0.7rem; padding: 0.25rem 0.5rem; }
206
+ .fp-sync-bar .sync-dir { font-size: 0.68rem; color: #8b949e; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
207
+ .fp-diff-item { display: flex; align-items: center; padding: 0.4rem 0.75rem; gap: 0.4rem; font-size: 0.78rem; color: #e6edf3; }
208
+ .fp-diff-item .badge { font-size: 0.65rem; padding: 0.1rem 0.35rem; border-radius: 4px; font-weight: 600; flex-shrink: 0; }
209
+ .badge-new { background: #238636; color: #fff; }
210
+ .badge-changed { background: #9e6a03; color: #fff; }
211
+ .badge-arrow { color: #8b949e; flex-shrink: 0; font-size: 0.7rem; }
212
+ .fp-diff-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
213
+ .fp-diff-summary { padding: 0.75rem; text-align: center; color: #8b949e; font-size: 0.8rem; }
214
+ .fp-diff-actions { display: flex; gap: 0.5rem; padding: 0.5rem 0.75rem; flex-shrink: 0; }
215
+ .fp-diff-actions .fp-btn { flex: 1; text-align: center; padding: 0.45rem; }
216
+ .fp-btn-primary { background: #238636; border-color: #2ea04380; }
217
+ .fp-btn-primary:active { background: #2ea043; }
218
+ .fp-btn-warn { background: #9e6a03; border-color: #d2992280; }
219
+ .fp-btn-warn:active { background: #bb8009; }
220
+
175
221
  /* ── 底部选择面板 ─────────────────────── */
176
222
  .action-sheet { display: none; position: fixed; inset: 0; z-index: 80; }
177
223
  .action-sheet.open { display: flex; align-items: flex-end; justify-content: center; }
@@ -212,6 +258,7 @@
212
258
  <div id="session-title">加载中...</div>
213
259
  <div id="session-cwd"></div>
214
260
  </div>
261
+ <button class="top-btn" onclick="openFilePanel()" title="文件" style="font-size:0.7rem;color:var(--muted)">文件</button>
215
262
  <button class="top-btn" onclick="openSettings()" title="设置">
216
263
  <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>
217
264
  </button>
@@ -290,6 +337,31 @@
290
337
  </div>
291
338
  </div>
292
339
 
340
+ <!-- 文件管理面板 -->
341
+ <div id="file-panel-backdrop" onclick="closeFilePanel()"></div>
342
+ <div id="file-panel">
343
+ <div class="fp-header">
344
+ <h3 id="fp-title">文件管理</h3>
345
+ <button class="fp-btn" onclick="fpUploadClick()">上传</button>
346
+ <button class="fp-btn" onclick="fpStartSync()">同步</button>
347
+ <button class="fp-btn" onclick="closeFilePanel()">关闭</button>
348
+ </div>
349
+ <div class="fp-sync-bar" id="fp-sync-bar" style="display:none">
350
+ <span class="sync-dir" id="fp-sync-dir"></span>
351
+ <button class="fp-btn" onclick="fpPickDir()">换目录</button>
352
+ <button class="fp-btn" onclick="fpExitSync()">退出同步</button>
353
+ </div>
354
+ <div class="fp-path" id="fp-path"></div>
355
+ <div class="fp-list" id="fp-list"></div>
356
+ <div class="fp-diff-actions" id="fp-diff-actions" style="display:none">
357
+ <button class="fp-btn fp-btn-primary" onclick="fpSyncToLocal()">下载到本地</button>
358
+ <button class="fp-btn fp-btn-warn" onclick="fpSyncToServer()">上传到服务器</button>
359
+ </div>
360
+ <div class="fp-progress" id="fp-progress"></div>
361
+ <div class="fp-drop-zone" id="fp-drop">拖拽文件到此处上传,或点击上方按钮</div>
362
+ <input type="file" id="fp-file-input" multiple style="display:none">
363
+ </div>
364
+
293
365
  <!-- 设置面板 -->
294
366
  <div id="settings-backdrop" onclick="closeSettings()"></div>
295
367
  <div id="settings-panel">
@@ -1327,6 +1399,324 @@
1327
1399
  updateShortcutBar();
1328
1400
  }
1329
1401
 
1402
+ // ── 文件管理 ──────────────────────────────────
1403
+ let fpCurrentPath = '';
1404
+ window.openFilePanel = function() {
1405
+ fpCurrentPath = '';
1406
+ fpLoadFiles();
1407
+ document.getElementById('file-panel').classList.add('open');
1408
+ document.getElementById('file-panel-backdrop').classList.add('open');
1409
+ };
1410
+ window.closeFilePanel = function() {
1411
+ document.getElementById('file-panel').classList.remove('open');
1412
+ document.getElementById('file-panel-backdrop').classList.remove('open');
1413
+ };
1414
+
1415
+ async function fpLoadFiles() {
1416
+ const list = document.getElementById('fp-list');
1417
+ list.innerHTML = '<div class="fp-empty">加载中...</div>';
1418
+ try {
1419
+ const r = await fetch(`/api/files/${SESSION_ID}?path=${encodeURIComponent(fpCurrentPath)}`);
1420
+ const data = await r.json();
1421
+ if (!data.ok) { list.innerHTML = `<div class="fp-empty">${escHtml(data.error)}</div>`; return; }
1422
+ document.getElementById('fp-path').textContent = data.cwd + (data.path ? '/' + data.path : '');
1423
+ if (!data.entries.length && data.parent == null) {
1424
+ list.innerHTML = '<div class="fp-empty">空目录</div>';
1425
+ return;
1426
+ }
1427
+ let html = '';
1428
+ if (data.parent != null) {
1429
+ html += `<div class="fp-item" onclick="fpNav('${escHtml(data.parent)}')"><span class="fp-icon">📁</span><span class="fp-name">..</span></div>`;
1430
+ }
1431
+ for (const e of data.entries) {
1432
+ const rel = fpCurrentPath ? fpCurrentPath + '/' + e.name : e.name;
1433
+ if (e.isDir) {
1434
+ html += `<div class="fp-item" onclick="fpNav('${escHtml(rel)}')"><span class="fp-icon">📁</span><span class="fp-name">${escHtml(e.name)}</span></div>`;
1435
+ } else {
1436
+ html += `<div class="fp-item">
1437
+ <span class="fp-icon">📄</span>
1438
+ <span class="fp-name">${escHtml(e.name)}</span>
1439
+ <span class="fp-size">${fpFormatSize(e.size)}</span>
1440
+ <a class="fp-dl" href="/api/files/${SESSION_ID}/download?path=${encodeURIComponent(rel)}" download title="下载">下载</a>
1441
+ </div>`;
1442
+ }
1443
+ }
1444
+ list.innerHTML = html;
1445
+ } catch (e) {
1446
+ list.innerHTML = `<div class="fp-empty">加载失败: ${escHtml(e.message)}</div>`;
1447
+ }
1448
+ }
1449
+
1450
+ window.fpNav = function(path) { fpCurrentPath = path; fpLoadFiles(); };
1451
+
1452
+ function fpFormatSize(bytes) {
1453
+ if (bytes == null) return '';
1454
+ if (bytes < 1024) return bytes + ' B';
1455
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
1456
+ if (bytes < 1024 * 1024 * 1024) return (bytes / 1024 / 1024).toFixed(1) + ' MB';
1457
+ return (bytes / 1024 / 1024 / 1024).toFixed(1) + ' GB';
1458
+ }
1459
+
1460
+ window.fpUploadClick = function() { document.getElementById('fp-file-input').click(); };
1461
+
1462
+ document.getElementById('fp-file-input').addEventListener('change', (e) => {
1463
+ if (e.target.files.length) fpUploadFiles(e.target.files);
1464
+ e.target.value = '';
1465
+ });
1466
+
1467
+ async function fpUploadFiles(files) {
1468
+ if (!files.length) return;
1469
+ const progress = document.getElementById('fp-progress');
1470
+ progress.style.display = 'block';
1471
+ progress.textContent = `正在上传 ${files.length} 个文件...`;
1472
+ try {
1473
+ const form = new FormData();
1474
+ for (const f of files) form.append('files', f, f.webkitRelativePath || f.name);
1475
+ const r = await fetch(`/api/files/${SESSION_ID}/upload?path=${encodeURIComponent(fpCurrentPath)}`, {
1476
+ method: 'POST', body: form,
1477
+ });
1478
+ const data = await r.json();
1479
+ if (data.ok) {
1480
+ progress.textContent = `已上传 ${data.files.length} 个文件`;
1481
+ fpLoadFiles();
1482
+ } else {
1483
+ progress.textContent = '上传失败: ' + (data.error || '未知错误');
1484
+ }
1485
+ } catch (e) {
1486
+ progress.textContent = '上传出错: ' + e.message;
1487
+ }
1488
+ setTimeout(() => { progress.style.display = 'none'; }, 3000);
1489
+ }
1490
+
1491
+ // 拖拽上传
1492
+ const fpDrop = document.getElementById('fp-drop');
1493
+ fpDrop.addEventListener('dragover', (e) => { e.preventDefault(); fpDrop.classList.add('dragover'); });
1494
+ fpDrop.addEventListener('dragleave', () => fpDrop.classList.remove('dragover'));
1495
+ fpDrop.addEventListener('drop', (e) => {
1496
+ e.preventDefault();
1497
+ fpDrop.classList.remove('dragover');
1498
+ if (e.dataTransfer.files.length) fpUploadFiles(e.dataTransfer.files);
1499
+ });
1500
+
1501
+ // 监听文件变更
1502
+ socket.on('files:changed', ({ sessionId }) => {
1503
+ if (sessionId === SESSION_ID && document.getElementById('file-panel').classList.contains('open')) {
1504
+ if (syncMode) fpComputeDiff();
1505
+ else fpLoadFiles();
1506
+ }
1507
+ });
1508
+
1509
+ // ── 文件同步(File System Access API)──────────
1510
+ let syncDirHandle = null;
1511
+ let syncMode = false;
1512
+ let syncDiff = { toDownload: [], toUpload: [] };
1513
+
1514
+ window.fpStartSync = async function() {
1515
+ if (!window.showDirectoryPicker) {
1516
+ alert('当前浏览器不支持文件夹访问,请使用 Chrome 或 Edge');
1517
+ return;
1518
+ }
1519
+ if (!syncDirHandle) {
1520
+ await fpPickDirInner();
1521
+ if (!syncDirHandle) return;
1522
+ }
1523
+ syncMode = true;
1524
+ document.getElementById('fp-sync-bar').style.display = 'flex';
1525
+ document.getElementById('fp-drop').style.display = 'none';
1526
+ await fpComputeDiff();
1527
+ };
1528
+
1529
+ window.fpPickDir = async function() { await fpPickDirInner(); };
1530
+ async function fpPickDirInner() {
1531
+ try {
1532
+ syncDirHandle = await window.showDirectoryPicker({ mode: 'readwrite' });
1533
+ document.getElementById('fp-sync-dir').textContent = syncDirHandle.name;
1534
+ if (syncMode) await fpComputeDiff();
1535
+ } catch (e) {
1536
+ if (e.name !== 'AbortError') alert('选择文件夹失败: ' + e.message);
1537
+ }
1538
+ }
1539
+
1540
+ window.fpExitSync = function() {
1541
+ syncMode = false;
1542
+ document.getElementById('fp-sync-bar').style.display = 'none';
1543
+ document.getElementById('fp-diff-actions').style.display = 'none';
1544
+ document.getElementById('fp-drop').style.display = '';
1545
+ fpLoadFiles();
1546
+ };
1547
+
1548
+ async function walkLocalDir(dirHandle, prefix) {
1549
+ const files = [];
1550
+ for await (const [name, handle] of dirHandle.entries()) {
1551
+ if (name.startsWith('.')) continue;
1552
+ const rel = prefix ? prefix + '/' + name : name;
1553
+ if (handle.kind === 'directory') {
1554
+ files.push(...await walkLocalDir(handle, rel));
1555
+ } else {
1556
+ const file = await handle.getFile();
1557
+ files.push({ name: rel, size: file.size, handle });
1558
+ }
1559
+ }
1560
+ return files;
1561
+ }
1562
+
1563
+ async function hashFile(file) {
1564
+ const buf = await file.arrayBuffer();
1565
+ const hash = await crypto.subtle.digest('SHA-256', buf);
1566
+ return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('');
1567
+ }
1568
+
1569
+ async function fpComputeDiff() {
1570
+ const list = document.getElementById('fp-list');
1571
+ list.innerHTML = '<div class="fp-empty">正在比对文件...</div>';
1572
+ const progress = document.getElementById('fp-progress');
1573
+ progress.style.display = 'block';
1574
+
1575
+ try {
1576
+ progress.textContent = '获取服务器文件清单...';
1577
+ const r = await fetch(`/api/files/${SESSION_ID}/manifest?path=${encodeURIComponent(fpCurrentPath)}`);
1578
+ const manifest = await r.json();
1579
+ if (!manifest.ok) throw new Error(manifest.error);
1580
+
1581
+ progress.textContent = '扫描本地文件...';
1582
+ const localFiles = await walkLocalDir(syncDirHandle, '');
1583
+
1584
+ const serverMap = new Map(manifest.files.map(f => [f.name, f]));
1585
+ const localMap = new Map(localFiles.map(f => [f.name, f]));
1586
+
1587
+ const toDownload = [];
1588
+ const toUpload = [];
1589
+
1590
+ let checked = 0;
1591
+ const total = serverMap.size + localMap.size;
1592
+ for (const [name, sf] of serverMap) {
1593
+ if (++checked % 20 === 0) progress.textContent = `比对中... ${checked}/${total}`;
1594
+ const lf = localMap.get(name);
1595
+ if (!lf) {
1596
+ toDownload.push({ name, size: sf.size, reason: 'new' });
1597
+ } else if (lf.size !== sf.size) {
1598
+ toDownload.push({ name, size: sf.size, reason: 'changed' });
1599
+ } else if (sf.hash && sf.size < 10 * 1024 * 1024) {
1600
+ const file = await lf.handle.getFile();
1601
+ const localHash = await hashFile(file);
1602
+ if (localHash !== sf.hash) toDownload.push({ name, size: sf.size, reason: 'changed' });
1603
+ }
1604
+ }
1605
+ for (const [name, lf] of localMap) {
1606
+ if (++checked % 20 === 0) progress.textContent = `比对中... ${checked}/${total}`;
1607
+ const sf = serverMap.get(name);
1608
+ if (!sf) {
1609
+ toUpload.push({ name, size: lf.size, reason: 'new', handle: lf.handle });
1610
+ } else if (lf.size !== sf.size) {
1611
+ toUpload.push({ name, size: lf.size, reason: 'changed', handle: lf.handle });
1612
+ } else if (sf.hash && lf.size < 10 * 1024 * 1024) {
1613
+ const file = await lf.handle.getFile();
1614
+ const localHash = await hashFile(file);
1615
+ if (localHash !== sf.hash) toUpload.push({ name, size: lf.size, reason: 'changed', handle: lf.handle });
1616
+ }
1617
+ }
1618
+
1619
+ syncDiff = { toDownload, toUpload };
1620
+ fpRenderDiff();
1621
+ } catch (e) {
1622
+ list.innerHTML = `<div class="fp-empty">比对失败: ${escHtml(e.message)}</div>`;
1623
+ }
1624
+ progress.style.display = 'none';
1625
+ }
1626
+
1627
+ function fpRenderDiff() {
1628
+ const { toDownload, toUpload } = syncDiff;
1629
+ const list = document.getElementById('fp-list');
1630
+
1631
+ if (!toDownload.length && !toUpload.length) {
1632
+ list.innerHTML = '<div class="fp-diff-summary">文件已同步,无差异</div>';
1633
+ document.getElementById('fp-diff-actions').style.display = 'none';
1634
+ return;
1635
+ }
1636
+
1637
+ let html = '<div class="fp-diff-summary">';
1638
+ if (toDownload.length) html += `${toDownload.length} 个文件需下载到本地`;
1639
+ if (toDownload.length && toUpload.length) html += ',';
1640
+ if (toUpload.length) html += `${toUpload.length} 个文件需上传到服务器`;
1641
+ html += '</div>';
1642
+
1643
+ for (const f of toDownload) {
1644
+ html += `<div class="fp-diff-item">
1645
+ <span class="badge ${f.reason === 'new' ? 'badge-new' : 'badge-changed'}">${f.reason === 'new' ? '新' : '改'}</span>
1646
+ <span class="badge-arrow">↓</span>
1647
+ <span class="fp-diff-name">${escHtml(f.name)}</span>
1648
+ <span class="fp-size">${fpFormatSize(f.size)}</span>
1649
+ </div>`;
1650
+ }
1651
+ for (const f of toUpload) {
1652
+ html += `<div class="fp-diff-item">
1653
+ <span class="badge ${f.reason === 'new' ? 'badge-new' : 'badge-changed'}">${f.reason === 'new' ? '新' : '改'}</span>
1654
+ <span class="badge-arrow">↑</span>
1655
+ <span class="fp-diff-name">${escHtml(f.name)}</span>
1656
+ <span class="fp-size">${fpFormatSize(f.size)}</span>
1657
+ </div>`;
1658
+ }
1659
+
1660
+ list.innerHTML = html;
1661
+ document.getElementById('fp-diff-actions').style.display = 'flex';
1662
+ }
1663
+
1664
+ window.fpSyncToLocal = async function() {
1665
+ const { toDownload } = syncDiff;
1666
+ if (!toDownload.length) return;
1667
+ const progress = document.getElementById('fp-progress');
1668
+ progress.style.display = 'block';
1669
+ let done = 0;
1670
+ for (const f of toDownload) {
1671
+ progress.textContent = `下载中 ${++done}/${toDownload.length}: ${f.name}`;
1672
+ try {
1673
+ const relPath = fpCurrentPath ? fpCurrentPath + '/' + f.name : f.name;
1674
+ const r = await fetch(`/api/files/${SESSION_ID}/download?path=${encodeURIComponent(relPath)}`);
1675
+ if (!r.ok) continue;
1676
+ const blob = await r.blob();
1677
+ const parts = f.name.split('/');
1678
+ let dir = syncDirHandle;
1679
+ for (let i = 0; i < parts.length - 1; i++) {
1680
+ dir = await dir.getDirectoryHandle(parts[i], { create: true });
1681
+ }
1682
+ const fh = await dir.getFileHandle(parts[parts.length - 1], { create: true });
1683
+ const w = await fh.createWritable();
1684
+ await w.write(blob);
1685
+ await w.close();
1686
+ } catch (e) { console.error(`同步下载失败 ${f.name}:`, e); }
1687
+ }
1688
+ progress.textContent = `已下载 ${done} 个文件到本地`;
1689
+ setTimeout(() => { progress.style.display = 'none'; }, 3000);
1690
+ await fpComputeDiff();
1691
+ };
1692
+
1693
+ window.fpSyncToServer = async function() {
1694
+ const { toUpload } = syncDiff;
1695
+ if (!toUpload.length) return;
1696
+ const progress = document.getElementById('fp-progress');
1697
+ progress.style.display = 'block';
1698
+ const BATCH = 20;
1699
+ let done = 0;
1700
+ for (let i = 0; i < toUpload.length; i += BATCH) {
1701
+ const batch = toUpload.slice(i, i + BATCH);
1702
+ progress.textContent = `上传中 ${done}/${toUpload.length}...`;
1703
+ const form = new FormData();
1704
+ for (const f of batch) {
1705
+ try { form.append('files', await f.handle.getFile(), f.name); }
1706
+ catch (e) { console.error(`读取失败 ${f.name}:`, e); }
1707
+ }
1708
+ try {
1709
+ await fetch(`/api/files/${SESSION_ID}/upload?path=${encodeURIComponent(fpCurrentPath)}`, {
1710
+ method: 'POST', body: form,
1711
+ });
1712
+ } catch (e) { console.error('上传批次失败:', e); }
1713
+ done += batch.length;
1714
+ }
1715
+ progress.textContent = `已上传 ${done} 个文件到服务器`;
1716
+ setTimeout(() => { progress.style.display = 'none'; }, 3000);
1717
+ await fpComputeDiff();
1718
+ };
1719
+
1330
1720
  // ── 设置面板 ──────────────────────────────────
1331
1721
  window.openSettings = function() { document.getElementById('settings-backdrop').classList.add('open'); document.getElementById('settings-panel').classList.add('open'); syncSettingsUI(); };
1332
1722
  window.closeSettings = function() { document.getElementById('settings-backdrop').classList.remove('open'); document.getElementById('settings-panel').classList.remove('open'); };