dsh-mcp-connector 0.2.71 → 0.2.72

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/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  本项目遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/);首个公开版本从 `0.1.0` 开始。
4
4
 
5
+ ## [0.2.72] - 2026-10-14
6
+
7
+ ### Fixed
8
+
9
+ - 宿主切换面板销毁 iframe 后,按工作区恢复页签、搜索、市场分类、展开章节、工具筛选/分页及列表滚动。状态仅保留在同源宿主当前页面内存,最多 32 个工作区,不保存凭据表单、API 返回或授权弹框;刷新宿主页面即清除。
10
+ - 市场与已安装列表加入请求序列和工作区检查,防止较早响应覆盖新页签/工作区;加载中的滚动归零不覆盖恢复位置。
11
+ - 本次为浏览状态恢复而非 iframe 保活:仍重新拉取数据、重建 SSE,详情和未完成表单不恢复。
12
+
5
13
  ## [0.2.71] - 2026-10-14
6
14
 
7
15
  ### Added
package/README.en.md CHANGED
@@ -215,7 +215,7 @@ npm run dev:ui
215
215
 
216
216
  Every Registry merge regenerates `catalog-stats.json`; an hourly workflow in this repository synchronizes the Chinese and English product copy plus a local stats snapshot. The static npm README updates with package releases, while the live badges above read the Registry directly and therefore stay current without another npm release.
217
217
 
218
- The current public version is [`dsh-mcp-connector@0.2.71`](https://www.npmjs.com/package/dsh-mcp-connector), with [GitHub Release v0.2.71](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.71).
218
+ The current public version is [`dsh-mcp-connector@0.2.72`](https://www.npmjs.com/package/dsh-mcp-connector), with [GitHub Release v0.2.72](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.72).
219
219
 
220
220
  See [CHANGELOG.md](CHANGELOG.md) for version history and [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md) for the Desktop release checklist.
221
221
 
package/README.md CHANGED
@@ -210,7 +210,7 @@ npm run dev:ui
210
210
 
211
211
  公共 Registry 每次合并后会生成 `catalog-stats.json`;本仓库的定时工作流每小时同步中英文介绍和统计快照。npm 页面中的静态正文随版本发布更新,上方动态统计徽标则直接读取 Registry,可在不发布新 npm 版本时保持实时数量一致。
212
212
 
213
- 当前公开版本为 [`dsh-mcp-connector@0.2.71`](https://www.npmjs.com/package/dsh-mcp-connector),对应 [GitHub Release v0.2.71](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.71)。
213
+ 当前公开版本为 [`dsh-mcp-connector@0.2.72`](https://www.npmjs.com/package/dsh-mcp-connector),对应 [GitHub Release v0.2.72](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.72)。
214
214
 
215
215
  版本能力与变更记录见 [CHANGELOG.md](CHANGELOG.md)。
216
216
  Desktop 发版回归见 [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md)。
@@ -23,6 +23,13 @@ npm run dev:ui
23
23
 
24
24
  ## 不自动执行的项目
25
25
 
26
+ ### 面板浏览状态恢复(#135)
27
+
28
+ - 在工作区 A 设置市场分类、搜索、展开章节并滚动;切换其他宿主面板再回来,检查恢复。已安装和工具页签(含筛选、分页)分别重复。
29
+ - 切换工作区 B 不应带入 A 的筛选;回到 A 应恢复 A 的状态。慢网络下快速切页签/工作区,旧响应不得覆盖新页面。
30
+ - 返回仍重新读取数据、重建 SSE;这是状态恢复,不是 iframe 常驻。详情、授权弹框、凭据及 JSON 编辑表单不恢复。
31
+ - 浏览状态只在同源宿主当前页面内存保留,最多 32 个工作区;刷新宿主后清空。跨源/受限父页面不可访问时降级为不跨 iframe 生命周期保存。
32
+
26
33
  - 不在 CI 中完成真实 OAuth 登录。
27
34
  - 不自动删除旧插件、旧 storage domain 或凭据。
28
35
  - 不因探针一次失败直接删除社区条目;定时报告供维护者复核。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-mcp-connector",
3
- "version": "0.2.71",
3
+ "version": "0.2.72",
4
4
  "description": "DeepSeek Harness MCP Connector: connect servers, search tools across active connections, and troubleshoot discovery. Includes a continuously updated catalog; supports OAuth 2.0 PKCE, API keys, Streamable HTTP/stdio, and mcpServers JSON import.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
package/ui/index.html CHANGED
@@ -628,6 +628,74 @@
628
628
  let pendingScopeKey = null;
629
629
  let installedItems = [];
630
630
 
631
+ // Same-tab memory only: no credentials, modal contents or API responses are retained.
632
+ // The Host owns this memory across iframe unmounts; reloading the Host clears it.
633
+ const viewStateMemory = (() => {
634
+ try {
635
+ const host = window.parent;
636
+ if (host.location.origin !== window.location.origin) return new Map();
637
+ const key = '__mcpConnectorViewStateV1';
638
+ if (!(host[key] instanceof host.Map)) host[key] = new host.Map();
639
+ return host[key];
640
+ } catch { return new Map(); }
641
+ })();
642
+ let viewStateReady = window.parent === window;
643
+ let viewRenderSequence = 0;
644
+ let viewStateLoading = false;
645
+ let pendingViewScroll = null;
646
+
647
+ function normalizeViewState(value) {
648
+ const text = (s) => typeof s === 'string' ? s.slice(0, 500) : '';
649
+ const category = (s) => CATEGORIES.some((item) => item.value === s) ? s : '';
650
+ return {
651
+ view: ['market', 'installed', 'tools'].includes(value?.view) ? value.view : 'market',
652
+ search: text(value?.search),
653
+ category: category(value?.category),
654
+ expanded: Array.isArray(value?.expanded) ? [...new Set(value.expanded.map(category).filter(Boolean))] : [],
655
+ filter: { connectionKey: text(value?.filter?.connectionKey), serverName: text(value?.filter?.serverName),
656
+ status: ['', 'pending', 'unknown', 'success', 'failed'].includes(value?.filter?.status) ? value.filter.status : '' },
657
+ offset: Number.isSafeInteger(value?.offset) && value.offset >= 0 ? Math.min(value.offset, 100000) : 0,
658
+ scroll: Number.isFinite(value?.scroll) ? Math.max(0, Math.min(value.scroll, 10000000)) : 0,
659
+ };
660
+ }
661
+
662
+ function viewStateKey() { return JSON.stringify([currentWorkspace?.workspaceId ?? null]); }
663
+
664
+ function saveViewState() {
665
+ if (!viewStateReady || viewStateLoading) return;
666
+ const key = viewStateKey();
667
+ viewStateMemory.delete(key);
668
+ viewStateMemory.set(key, normalizeViewState({ view, search: searchQuery, category: marketCategory,
669
+ expanded: [...expandedMarketSections], filter: explorerFilter, offset: explorerOffset, scroll: main.scrollTop }));
670
+ while (viewStateMemory.size > 32) viewStateMemory.delete(viewStateMemory.keys().next().value);
671
+ }
672
+
673
+ function restoreViewState() {
674
+ const state = normalizeViewState(viewStateMemory.get(viewStateKey()));
675
+ view = state.view; searchQuery = state.search; searchInput.value = searchQuery;
676
+ marketCategory = state.category;
677
+ expandedMarketSections.clear(); state.expanded.forEach((value) => expandedMarketSections.add(value));
678
+ explorerFilter = state.filter; explorerOffset = state.offset;
679
+ pendingViewScroll = state.scroll;
680
+ syncTabs();
681
+ }
682
+
683
+ function beginViewRender() {
684
+ if (pendingViewScroll === null) saveViewState();
685
+ viewStateLoading = true;
686
+ return { sequence: ++viewRenderSequence, workspace: viewStateKey(), view };
687
+ }
688
+
689
+ function isViewRenderCurrent(render) {
690
+ return render.sequence === viewRenderSequence && render.workspace === viewStateKey() && render.view === view;
691
+ }
692
+
693
+ function finishViewRender(render) {
694
+ if (!isViewRenderCurrent(render)) return;
695
+ if (pendingViewScroll !== null) { main.scrollTop = pendingViewScroll; pendingViewScroll = null; }
696
+ viewStateLoading = false;
697
+ }
698
+
631
699
  async function call(method, params, { timeoutMs: timeoutOverride } = {}) {
632
700
  const controller = new AbortController();
633
701
  const timeoutMs = timeoutOverride || API_TIMEOUTS[method] || DEFAULT_API_TIMEOUT_MS;
@@ -716,16 +784,20 @@
716
784
  const workspace = event.data.workspace;
717
785
  const previousWorkspaceId = currentWorkspace?.workspaceId ?? null;
718
786
  const previousWorkspaceOptions = JSON.stringify(availableWorkspaces);
787
+ const firstContext = !viewStateReady;
788
+ saveViewState();
719
789
  currentWorkspace = workspace && typeof workspace.workspaceId === 'string'
720
790
  ? { workspaceId: workspace.workspaceId, title: String(workspace.title || workspace.workspaceId) }
721
791
  : null;
722
792
  updateAvailableWorkspaces(event.data.workspaces);
723
- if ((currentWorkspace?.workspaceId ?? null) !== previousWorkspaceId || JSON.stringify(availableWorkspaces) !== previousWorkspaceOptions) {
793
+ if (firstContext || (currentWorkspace?.workspaceId ?? null) !== previousWorkspaceId || JSON.stringify(availableWorkspaces) !== previousWorkspaceOptions) {
724
794
  explorerSequence++; explorerDetailSequence++;
725
795
  explorerData = null; explorerOffset = 0;
726
796
  explorerFilter = { connectionKey: '', serverName: '', status: '' };
727
797
  closeModal(); closeDetail();
728
- if (view === 'tools') main.innerHTML = '';
798
+ viewStateReady = true;
799
+ if (firstContext || (currentWorkspace?.workspaceId ?? null) !== previousWorkspaceId) restoreViewState();
800
+ main.innerHTML = '';
729
801
  refresh();
730
802
  }
731
803
  return;
@@ -1275,10 +1347,13 @@
1275
1347
  }
1276
1348
 
1277
1349
  async function loadMarket({ checkHealth = true, preserveScroll = false } = {}) {
1350
+ const render = beginViewRender();
1351
+ try {
1278
1352
  const previousScrollTop = preserveScroll ? main.scrollTop : 0;
1279
1353
  syncMarketNav();
1280
1354
  main.innerHTML = '<div class="empty">加载中…</div>';
1281
1355
  const [r, status] = await Promise.all([call('catalog', workspaceParams()), call('status', workspaceParams())]);
1356
+ if (!isViewRenderCurrent(render)) return;
1282
1357
  if (status.ok) {
1283
1358
  updateAvailableWorkspaces(status.detail?.scope?.workspaces);
1284
1359
  setTabCount('installed', (status.detail?.items ?? []).length);
@@ -1300,9 +1375,10 @@
1300
1375
  if (checkHealth && !initialHealthCheckStarted && items.some((item) => item.connected?.length > 0)) {
1301
1376
  initialHealthCheckStarted = true;
1302
1377
  call('healthCheck', {}).then(() => {
1303
- if (view === 'market') loadMarket({ checkHealth: false });
1378
+ if (view === 'market') loadMarket({ checkHealth: false, preserveScroll: true });
1304
1379
  });
1305
1380
  }
1381
+ } finally { finishViewRender(render); }
1306
1382
  }
1307
1383
 
1308
1384
  function connectionLabels(d) {
@@ -1370,6 +1446,8 @@
1370
1446
  const formatObserved = (value) => value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '尚无记录';
1371
1447
 
1372
1448
  async function loadToolExplorer() {
1449
+ const render = beginViewRender();
1450
+ try {
1373
1451
  const sequence = ++explorerSequence;
1374
1452
  const workspaceId = currentWorkspace?.workspaceId;
1375
1453
  syncMarketNav();
@@ -1377,7 +1455,7 @@
1377
1455
  main.innerHTML = `<section id="tool-explorer"><p class="hint">搜索当前范围内已启用连接的工具缓存;不会执行工具。最近发现成功不代表每次调用均可成功。</p><div class="explorer-filters"><label>连接<select id="explorer-connection" aria-label="按连接筛选"></select></label><label>服务<select id="explorer-server" aria-label="按服务筛选"></select></label><label>发现状态<select id="explorer-status" aria-label="按发现状态筛选"><option value="">全部状态</option>${Object.entries(EXPLORER_STATUS).map(([key, label]) => `<option value="${key}">${label}</option>`).join('')}</select></label><button class="btn ghost" data-explorer-clear>清除筛选</button></div><p id="explorer-summary" role="status">正在读取缓存…</p><div id="explorer-connections"></div><div id="explorer-results"></div><div id="explorer-pages" class="m-actions"></div></section>`;
1378
1456
  }
1379
1457
  const response = await call('toolExplorer', { ...workspaceParams(), ...explorerFilter, query: searchQuery, offset: explorerOffset, limit: 20 });
1380
- if (sequence !== explorerSequence || view !== 'tools' || workspaceId !== currentWorkspace?.workspaceId) return;
1458
+ if (!isViewRenderCurrent(render) || sequence !== explorerSequence || view !== 'tools' || workspaceId !== currentWorkspace?.workspaceId) return;
1381
1459
  if (!response.ok) { $('#explorer-summary').textContent = response.message; return; }
1382
1460
  explorerData = response.detail;
1383
1461
  const { connections = [], selectedConnections = [], items = [], total = 0 } = explorerData;
@@ -1392,8 +1470,10 @@
1392
1470
  $('#explorer-connections').innerHTML = `<details ${explorerFilter.connectionKey || !total ? 'open' : ''}><summary>连接状态与故障处理(${selectedConnections.length})</summary>${selectedConnections.map((c) => `<div class="explorer-card"><strong>${esc(c.connectionName)}</strong><div>${esc(c.serverName)} · ${esc(EXPLORER_STATUS[c.status] || c.status)} · 缓存 ${c.toolCount} 个工具</div><div class="hint">最近发现:${esc(formatObserved(c.checkedAt))};最后成功:${esc(formatObserved(c.observedAt))}${c.stale ? ' · 缓存超过 24 小时' : ''}${c.cacheTruncated ? ' · 缓存清单已截断' : ''}</div>${c.diagnostic ? `<div>${esc(c.diagnostic.stageLabel)}:${esc(c.diagnostic.message)}<br>建议:${esc(c.diagnostic.action)}</div>` : ''}${c.retryReason === 'auth' ? '<p>鉴权失败已暂停后台重试,请到已安装页更新凭据或重新授权。</p>' : c.nextDiscoveryAt ? `<p class="hint">下次后台发现不早于:${esc(formatObserved(c.nextDiscoveryAt))}(页面保持连接时)</p>` : ''}<div class="m-actions"><button class="btn ghost" data-explorer-action="discover" data-key="${esc(c.connectionKey)}" ${explorerBusy.has(c.connectionKey) ? 'disabled' : ''}>重新发现工具</button><button class="btn ghost" data-explorer-action="check" data-key="${esc(c.connectionKey)}" ${explorerBusy.has(c.connectionKey) ? 'disabled' : ''}>检查连接</button></div></div>`).join('') || '<p>当前筛选下没有连接。</p>'}</details>`;
1393
1471
  $('#explorer-results').innerHTML = items.map((item, index) => `<article class="explorer-card"><strong>${esc(item.name)}</strong>${item.title !== item.name ? `<div>${esc(item.title)}</div>` : ''}<p>${esc(item.description || '暂无描述')}</p><div class="hint">来源:${esc(item.connection.connectorName)} / ${esc(item.connection.connectionName)} / ${esc(item.serverName)}<br>${esc(EXPLORER_STATUS[item.connection.status])} · 最后成功缓存 ${esc(formatObserved(item.observedAt))}${item.stale ? ' · 已超过 24 小时' : ''}</div><button class="btn ghost" data-explorer-detail="${index}">参数详情</button></article>`).join('') || `<p class="empty">${connections.length ? '没有匹配工具。请调整关键词或筛选;也可展开连接状态检查是否尚未发现或读取失败。' : '当前范围没有已启用连接。请先添加连接,或选择对应工作区。'}</p>`;
1394
1472
  $('#explorer-pages').innerHTML = `<button class="btn ghost" data-explorer-page="prev" ${explorerOffset === 0 ? 'disabled' : ''}>上一页</button><span>${Math.floor(explorerOffset / 20) + 1} / ${Math.max(1, Math.ceil(total / 20))}</span><button class="btn ghost" data-explorer-page="next" ${explorerOffset + 20 >= total ? 'disabled' : ''}>下一页</button>`;
1473
+ } finally { finishViewRender(render); }
1395
1474
  }
1396
1475
 
1476
+ main.addEventListener('scroll', () => saveViewState(), { passive: true });
1397
1477
  main.addEventListener('change', (event) => {
1398
1478
  const fields = { 'explorer-connection': 'connectionKey', 'explorer-server': 'serverName', 'explorer-status': 'status' };
1399
1479
  const field = fields[event.target.id];
@@ -1429,10 +1509,13 @@
1429
1509
  });
1430
1510
 
1431
1511
  async function loadInstalled({ preserveScroll = false } = {}) {
1512
+ const render = beginViewRender();
1513
+ try {
1432
1514
  const previousScrollTop = preserveScroll ? main.scrollTop : 0;
1433
1515
  syncMarketNav();
1434
1516
  main.innerHTML = '<div class="empty">加载中…</div>';
1435
1517
  const [r, migration] = await Promise.all([call('status', workspaceParams()), call('migrationPreview', { scanStored: false })]);
1518
+ if (!isViewRenderCurrent(render)) return;
1436
1519
  if (!r.ok) {
1437
1520
  main.innerHTML = `<div class="empty">${esc(r.message)}</div>`;
1438
1521
  if (preserveScroll) main.scrollTop = previousScrollTop;
@@ -1456,6 +1539,7 @@
1456
1539
  const rollbackDisabled = scopeRollbackRevision === null ? 'disabled' : '';
1457
1540
  main.innerHTML = `${migrationHtml}<div class="stats"><span class="count">共 <strong>${filtered.length}</strong> 条连接${searchQuery ? ` · 搜索「${esc(searchQuery)}」` : ''} · 作用域 revision=${scopeRevision}</span><button class="btn ghost" id="scope-rollback" type="button" ${rollbackDisabled} onclick="window.__mcp.rollbackScope()">回滚上次范围变更</button><button class="btn ghost" id="config-backup" type="button" onclick="window.__mcp.openConfigBackup()">配置备份</button><button class="btn ghost" type="button" title="扫描企查查旧 OAuth 插件;不会删除旧插件或原凭据" onclick="window.__mcp.scanLegacy()">检测企查查旧授权</button></div><div class="rows">${filtered.map(rowHtml).join('')}</div>${communityCtaHtml()}`;
1458
1541
  if (preserveScroll) main.scrollTop = previousScrollTop;
1542
+ } finally { finishViewRender(render); }
1459
1543
  }
1460
1544
 
1461
1545
  function rowHtml(r) {
@@ -2425,6 +2509,7 @@
2425
2509
  }
2426
2510
  });
2427
2511
  window.addEventListener('pagehide', () => {
2512
+ saveViewState();
2428
2513
  notifyDetailState(false);
2429
2514
  clearTimeout(statusRefreshTimer);
2430
2515
  statusEventSource?.close();
@@ -2445,6 +2530,7 @@
2445
2530
  const categoryFilter = event.target.closest('[data-category-filter]');
2446
2531
  if (categoryFilter) {
2447
2532
  marketCategory = categoryFilter.dataset.categoryFilter || '';
2533
+ pendingViewScroll = null; main.scrollTop = 0;
2448
2534
  loadMarket({ checkHealth: false });
2449
2535
  }
2450
2536
  });
@@ -2472,6 +2558,7 @@
2472
2558
  const btn = e.target.closest('button[data-view]');
2473
2559
  if (!btn) return;
2474
2560
  view = btn.dataset.view;
2561
+ pendingViewScroll = null; main.scrollTop = 0;
2475
2562
  explorerSequence++; explorerDetailSequence++;
2476
2563
  syncTabs();
2477
2564
  refresh();
@@ -2479,6 +2566,7 @@
2479
2566
 
2480
2567
  searchInput.addEventListener('input', (e) => {
2481
2568
  searchQuery = e.target.value.trim();
2569
+ pendingViewScroll = null; main.scrollTop = 0;
2482
2570
  explorerSequence++; explorerOffset = 0;
2483
2571
  clearTimeout(searchTimer);
2484
2572
  if (view === 'tools') { searchTimer = setTimeout(() => { if (view === 'tools') void loadToolExplorer(); }, 200); return; }
@@ -2486,6 +2574,7 @@
2486
2574
  refresh();
2487
2575
  });
2488
2576
 
2577
+ if (viewStateReady) restoreViewState();
2489
2578
  requestWorkspaceContext();
2490
2579
  connectStatusEvents();
2491
2580
  refresh();