dsh-mcp-connector 0.2.14 → 0.2.16

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
@@ -4,6 +4,28 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.2.16] - 2026-08-24
8
+
9
+ ### Changed
10
+
11
+ - 市场分类栏移入固定 Header 第二行,不再悬浮覆盖滚动中的卡片;切换到“已安装”时自动隐藏并收回空间。
12
+ - 移除市场正文中的冗余连接器数量与操作提示,把市场/已安装总数改为页签内的轻量徽标。
13
+
14
+ ### Verification
15
+
16
+ - 插件 88 项自动测试、lint 与 npm 发布文件白名单/敏感内容扫描通过。
17
+ - 使用 UI harness 验证桌面分类标签无横向溢出;滚动后 Header 与分类栏坐标保持不变,分类点击与已安装页签切换正常。
18
+
19
+ ## [0.2.15] - 2026-08-24
20
+
21
+ ### Fixed
22
+
23
+ - 未连接且目录没有工具快照的卡片详情不再误报“当前连接异常”;现在保持未连接状态,并提示连接后读取服务端工具清单。
24
+
25
+ ### Verification
26
+
27
+ - 新增详情状态回归,插件自动测试增至 87 项;使用本机 DSH 对 Seedream 未配置详情进行实机验收。
28
+
7
29
  ## [0.2.14] - 2026-08-24
8
30
 
9
31
  ### Added
package/README.en.md CHANGED
@@ -101,7 +101,7 @@ npm run dev:ui
101
101
 
102
102
  `npm run check` performs syntax checks, automated tests, and an npm package allowlist/sensitive-content audit. `npm run market:check` tracks the external DSH marketplace PR and live directory. Tags matching `v*` trigger GitHub Actions; the tag must match `package.json`. npm releases use Trusted Publishing through GitHub OIDC and do not require a long-lived `NPM_TOKEN`.
103
103
 
104
- The current public version is [`dsh-mcp-connector@0.2.14`](https://www.npmjs.com/package/dsh-mcp-connector), with [GitHub Release v0.2.14](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.14).
104
+ The current public version is [`dsh-mcp-connector@0.2.16`](https://www.npmjs.com/package/dsh-mcp-connector), with [GitHub Release v0.2.16](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.16).
105
105
 
106
106
  See [CHANGELOG.md](CHANGELOG.md) for version history and [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md) for the Desktop release checklist.
107
107
 
package/README.md CHANGED
@@ -96,7 +96,7 @@ npm run dev:ui
96
96
 
97
97
  `check` 执行语法检查、自动测试和 npm 发布包白名单校验;`market:check` 检查外部 DSH 市场 PR 与线上目录;`dev:ui` 启动不含真实凭据的本地 mock 市场。CI 使用 `--legacy-peer-deps` 安装显式测试依赖,DSH 运行期 peer 仍由 Host 提供。`v*` Tag 会触发 GitHub Actions;Tag 必须与 `package.json` 版本一致。Release 通过 npm Trusted Publishing (GitHub OIDC) 发布,不依赖长期 `NPM_TOKEN`。
98
98
 
99
- 当前公开版本为 [`dsh-mcp-connector@0.2.14`](https://www.npmjs.com/package/dsh-mcp-connector),对应 [GitHub Release v0.2.14](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.14)。
99
+ 当前公开版本为 [`dsh-mcp-connector@0.2.16`](https://www.npmjs.com/package/dsh-mcp-connector),对应 [GitHub Release v0.2.16](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.16)。
100
100
 
101
101
  版本能力与变更记录见 [CHANGELOG.md](CHANGELOG.md)。
102
102
  Desktop 发版回归见 [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-mcp-connector",
3
- "version": "0.2.14",
3
+ "version": "0.2.16",
4
4
  "description": "General-purpose MCP connector, connection manager, plugin extension, and integration marketplace for DeepSeek Harness, initiated and maintained by Qichacha/QCC.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
package/ui/index.html CHANGED
@@ -61,7 +61,7 @@
61
61
 
62
62
  /* Header */
63
63
  header {
64
- display: flex; align-items: center; gap: 12px;
64
+ display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
65
65
  padding: 16px 24px; border-bottom: 1px solid var(--line);
66
66
  background: var(--card); position: sticky; top: 0; z-index: 5;
67
67
  flex-shrink: 0;
@@ -70,12 +70,20 @@
70
70
  header .logo .puzzle { font-size: 22px; }
71
71
  .tabs { display: flex; gap: 0; background: var(--bg); padding: 3px; border-radius: 10px; }
72
72
  .tabs button {
73
+ display: inline-flex; align-items: center; gap: 5px;
73
74
  border: 0; background: transparent; color: var(--text-2);
74
75
  padding: 6px 16px; border-radius: 8px; cursor: pointer; font-size: 13px; font-weight: 500;
75
76
  transition: all .15s;
76
77
  }
77
78
  .tabs button:hover { color: var(--text); }
78
79
  .tabs button.active { background: var(--card); color: var(--accent); box-shadow: var(--shadow); font-weight: 600; }
80
+ .tab-count {
81
+ min-width: 18px; height: 18px; padding: 0 5px; border-radius: 999px;
82
+ display: inline-flex; align-items: center; justify-content: center;
83
+ background: var(--line); color: var(--text-2); font-size: 10px; line-height: 1;
84
+ }
85
+ .tabs button.active .tab-count { background: var(--accent-light); color: var(--accent); }
86
+ .tab-count:empty { display: none; }
79
87
  .header-spacer { flex: 1; }
80
88
  .search-box {
81
89
  display: flex; align-items: center; gap: 8px;
@@ -104,13 +112,8 @@
104
112
  .stats { display: flex; align-items: center; gap: 16px; margin-bottom: 20px; }
105
113
  .stats .count { font-size: 13px; color: var(--text-2); }
106
114
  .stats .count strong { color: var(--text); font-weight: 600; }
107
- .market-filters {
108
- position: sticky; top: 0; z-index: 4;
109
- display: flex; align-items: center;
110
- margin: -8px 0 14px; padding: 10px 12px;
111
- background: var(--card); border: 1px solid var(--line); border-radius: 10px;
112
- box-shadow: 0 8px 14px color-mix(in srgb, var(--bg) 88%, transparent);
113
- }
115
+ .market-nav { flex: 0 0 100%; min-width: 0; order: 4; }
116
+ .market-filters { position: static; display: flex; align-items: center; }
114
117
  .filter-chips { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
115
118
  .filter-chip {
116
119
  border: 1px solid var(--line); background: var(--bg); color: var(--text-2);
@@ -365,7 +368,7 @@
365
368
  .inline-error.show { display: block; }
366
369
 
367
370
  @media (max-width: 620px) {
368
- header { padding: 12px; flex-wrap: wrap; }
371
+ header { padding: 12px; }
369
372
  .header-spacer { display: none; }
370
373
  .search-box { order: 3; width: 100%; }
371
374
  main { padding: 14px; }
@@ -388,8 +391,8 @@
388
391
  <div class="app">
389
392
  <header>
390
393
  <div class="tabs" id="tabs">
391
- <button data-view="market" class="active">市场</button>
392
- <button data-view="installed">已安装</button>
394
+ <button data-view="market" class="active">市场 <span class="tab-count" id="market-tab-count"></span></button>
395
+ <button data-view="installed">已安装 <span class="tab-count" id="installed-tab-count"></span></button>
393
396
  </div>
394
397
  <div class="header-spacer"></div>
395
398
  <div class="search-box">
@@ -398,6 +401,7 @@
398
401
  </div>
399
402
  <button class="icon-btn primary" id="add-connection" type="button">+ 添加连接</button>
400
403
  <button class="icon-btn" id="refresh">刷新</button>
404
+ <div class="market-nav" id="market-nav" aria-label="市场分类导航"></div>
401
405
  </header>
402
406
  <main id="main"></main>
403
407
 
@@ -742,6 +746,18 @@
742
746
  return `<div class="market-filters" role="group" aria-label="市场分类"><div class="filter-chips category-chips" role="group" aria-label="按分类筛选">${categoryChips}</div></div>`;
743
747
  }
744
748
 
749
+ function syncMarketNav() {
750
+ const nav = $('#market-nav');
751
+ nav.hidden = view !== 'market';
752
+ nav.innerHTML = view === 'market' ? marketFilterHtml() : '';
753
+ }
754
+
755
+ function setTabCount(tab, count) {
756
+ const node = $(`#${tab}-tab-count`);
757
+ node.textContent = Number.isFinite(count) ? String(count) : '';
758
+ node.setAttribute('aria-label', Number.isFinite(count) ? `${count} 个` : '');
759
+ }
760
+
745
761
  function marketSectionHtml(value, label, items) {
746
762
  if (!items.length) return '';
747
763
  const expanded = expandedMarketSections.has(value);
@@ -767,23 +783,23 @@
767
783
 
768
784
  async function loadMarket({ checkHealth = true, preserveScroll = false } = {}) {
769
785
  const previousScrollTop = preserveScroll ? main.scrollTop : 0;
786
+ syncMarketNav();
770
787
  main.innerHTML = '<div class="empty">加载中…</div>';
771
- const r = await call('catalog', {});
788
+ const [r, status] = await Promise.all([call('catalog', {}), call('status', {})]);
789
+ if (status.ok) setTabCount('installed', (status.detail?.items ?? []).length);
772
790
  if (!r.ok) { main.innerHTML = `<div class="empty">${esc(r.message)}</div>`; return; }
773
791
  const items = r.detail?.items ?? [];
792
+ setTabCount('market', items.length);
774
793
  if (!items.length) { main.innerHTML = '<div class="empty">暂无已上架的连接器</div>'; return; }
775
794
  marketItems = {};
776
795
  items.forEach((d) => { marketItems[d.id] = d; });
777
796
  const filtered = items.filter(matchesMarketFilters);
778
- const count = marketCategory ? filtered.length : items.filter(matchesSearch).length;
779
- const categoryLabel = CATEGORIES.find((cat) => cat.value === marketCategory)?.label;
780
- const filterSummary = marketCategory ? ` · 分类「${esc(categoryLabel)}」` : '';
781
797
  const resultHtml = !marketCategory
782
798
  ? groupedMarketHtml(items)
783
799
  : filtered.length > 0
784
800
  ? `<div class="grid">${filtered.map(cardHtml).join('')}</div>`
785
801
  : '<div class="empty">没有匹配当前筛选条件的连接器</div>';
786
- main.innerHTML = `${marketFilterHtml()}<div class="stats"><span class="count">共 <strong>${count}</strong> 个连接器${searchQuery ? ` · 搜索「${esc(searchQuery)}」` : ''}${filterSummary} · 点击卡片查看能力,点击「连接」完成授权</span></div>${resultHtml}`;
802
+ main.innerHTML = resultHtml;
787
803
  if (preserveScroll) main.scrollTop = previousScrollTop;
788
804
  if (checkHealth && !initialHealthCheckStarted && items.some((item) => item.connected?.length > 0)) {
789
805
  initialHealthCheckStarted = true;
@@ -831,10 +847,12 @@
831
847
  }
832
848
 
833
849
  async function loadInstalled() {
850
+ syncMarketNav();
834
851
  main.innerHTML = '<div class="empty">加载中…</div>';
835
852
  const [r, migration] = await Promise.all([call('status', {}), call('migrationPreview', { scanStored: false })]);
836
853
  if (!r.ok) { main.innerHTML = `<div class="empty">${esc(r.message)}</div>`; return; }
837
854
  const items = r.detail?.items ?? [];
855
+ setTabCount('installed', items.length);
838
856
  const pendingMigration = migration.ok ? migration.detail?.pendingCount || 0 : 0;
839
857
  const migrationHtml = pendingMigration > 0 ? `<div class="migration-banner"><div class="copy"><strong>发现旧企查查 MCP 授权</strong>可安全复制 ${pendingMigration} 组授权到 MCP连接器;不会删除旧插件或原凭据。</div><button class="btn" type="button" onclick="window.__mcp.migrateLegacy()">迁移</button></div>` : '';
840
858
  if (!items.length) { main.innerHTML = `${migrationHtml}<div class="stats"><span class="count">可从旧企查查 OAuth 插件复制已有授权</span><button class="btn ghost" type="button" onclick="window.__mcp.scanLegacy()">检测旧插件</button></div><div class="empty">还没有连接任何 MCP · 到「市场」里连接一个吧</div>`; return; }
@@ -1169,6 +1187,12 @@
1169
1187
  $('#detail-try-btn').textContent = detailNeedsReconfigure
1170
1188
  ? d.authMode === 'oauth2-pkce' && detailFailureState === 'reauth' ? '🔐 重新授权' : needsCredential && detailFailureState === 'reauth' ? t('reconfigureCredential') : '🔄 重新检查'
1171
1189
  : d.connected?.length > 0 ? t('try') : needsCredential ? t('configureConnect') : t('connectTry');
1190
+ if (!d.connected?.length && !d.toolsSnapshot?.length) {
1191
+ $('#tools-summary').textContent = '连接后查看工具';
1192
+ $('#detail-tools').innerHTML = '<div class="tools-empty">连接后可读取服务端工具清单</div>';
1193
+ renderPromptPage();
1194
+ return;
1195
+ }
1172
1196
  const toolsResult = await call('toolsList', { connectorId });
1173
1197
  if (loadId !== detailLoadId || !$('#detail-overlay').classList.contains('active')) return;
1174
1198
  if (!toolsResult.ok) {
@@ -1329,16 +1353,18 @@
1329
1353
  function refresh() { return view === 'market' ? loadMarket() : loadInstalled(); }
1330
1354
  function syncTabs() {
1331
1355
  document.querySelectorAll('#tabs button').forEach((button) => button.classList.toggle('active', button.dataset.view === view));
1356
+ syncMarketNav();
1332
1357
  }
1333
1358
 
1334
1359
  $('#add-connection').addEventListener('click', () => openAddConnection('json'));
1335
- main.addEventListener('click', (event) => {
1360
+ $('#market-nav').addEventListener('click', (event) => {
1336
1361
  const categoryFilter = event.target.closest('[data-category-filter]');
1337
1362
  if (categoryFilter) {
1338
1363
  marketCategory = categoryFilter.dataset.categoryFilter || '';
1339
1364
  loadMarket({ checkHealth: false });
1340
- return;
1341
1365
  }
1366
+ });
1367
+ main.addEventListener('click', (event) => {
1342
1368
  const sectionToggle = event.target.closest('[data-section-toggle]');
1343
1369
  if (sectionToggle) {
1344
1370
  const section = sectionToggle.dataset.sectionToggle;