dsh-mcp-connector 0.2.12 → 0.2.14

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,40 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.2.14] - 2026-08-24
8
+
9
+ ### Added
10
+
11
+ - 默认“全部”市场改为“推荐 + 9 类业务分类”的章节式浏览,每章默认展示 4 张卡片并支持就地“查看全部 / 收起”。
12
+ - 独立 Registry 新增由 Ace Data Cloud 托管的 Seedream 图片生成连接器,补齐“设计创意”分类;卡片明确第三方服务商、Bearer Token 与潜在计费边界。
13
+
14
+ ### Changed
15
+
16
+ - 分类栏固定在市场滚动区域顶部;桌面端全部标签单行显示且不再出现横向滚动条,窄屏改为自动换行。
17
+ - 推荐位严格收敛为 4 张企查查卡片、北大法宝和 Wind,共 6 张;其他第三方连接器仍按业务分类展示。
18
+ - “企查查·法律数据”迁入“法律合规”,“企查查·文档报告”迁入“效率工具”并保持该分类首位。
19
+
20
+ ### Verification
21
+
22
+ - 插件 86 项自动测试通过;Registry 10 项测试、10 条连接器构建和 schema/密钥审计通过。
23
+ - Seedream Hosted MCP 公网无凭据探针通过:端点可达并识别 MCP,正确返回 HTTP 401 鉴权挑战;未使用用户 Token,也未执行计费工具。
24
+
25
+ ## [0.2.13] - 2026-08-24
26
+
27
+ ### Added
28
+
29
+ - 当前市场已上架连接器全部进入推荐位;独立 Registry 新增 GitHub、Cloudflare、Notion、Tavily 四张经过官方配置与公开端点探针核验的推荐卡片。
30
+
31
+ ### Changed
32
+
33
+ - 市场筛选精简为“全部 / 推荐 / 业务分类”,移除服务商下拉框和 OAuth、Key / Token、免密接入方式筛选,降低首屏信息密度。
34
+ - 独立 Registry 的北大法宝、Wind、八爪鱼、QVeris、盈米统一使用标准分类并进入推荐位。
35
+
36
+ ### Verification
37
+
38
+ - 84 项插件自动测试、lint、内置目录校验与 45 个 npm 发布文件白名单/敏感内容扫描通过。
39
+ - Registry 9 项测试、9 条连接器构建、schema 校验与远程图标检查通过;GitHub、Cloudflare、Notion、Tavily 均完成 MCP 端点与鉴权元数据探针,未代替用户执行 OAuth 登录或授权后工具调用。
40
+
7
41
  ## [0.2.12] - 2026-08-23
8
42
 
9
43
  ### Added
package/README.en.md CHANGED
@@ -15,7 +15,7 @@ Browse and install MCP connectors from different providers in DeepSeek Harness D
15
15
  ## Features
16
16
 
17
17
  - A primary sidebar entry below New Conversation and above workspaces/conversations, with a public footer-slot fallback for incompatible DSH DOM versions.
18
- - Searchable Marketplace and Installed views, featured and nine-category filters, provider and connection-method filters, catalog refresh, and active health checks that distinguish Configured, Connected, Reauthorization Required, Partially Unavailable, and Connection Error states.
18
+ - Searchable Marketplace and Installed views; the default Marketplace groups cards into Featured plus nine business-category sections, previews four cards per section, keeps the category bar visible while scrolling, and shows every card when a single category is selected.
19
19
  - OAuth 2.0 Authorization Code with PKCE, API key/Bearer/unauthenticated HTTP configuration, stdio local-process configuration, and `mcpServers` JSON import.
20
20
  - Installation from a credential-free connector descriptor URL.
21
21
  - Credential and MCP initialize validation before API-key connectors are saved as installed.
@@ -26,7 +26,7 @@ Browse and install MCP connectors from different providers in DeepSeek Harness D
26
26
  - A standalone remote Registry, allowing new marketplace cards to appear after refresh without publishing a new npm version.
27
27
  - Explicit, non-destructive migration of authorization from the two earlier Qichacha OAuth plugins.
28
28
 
29
- The first four bundled cards are Qichacha connectors, followed by PKULaw and Wind. After a remote Registry refresh, Yingmi, QVeris, and Bazhuayu Cloud Collection are also available, for nine published marketplace cards in total. Bazhuayu is the first third-party example to exercise the complete OAuth 2.1 + PKCE dynamic-registration flow; the architecture remains provider-neutral.
29
+ The first four bundled cards are Qichacha connectors, followed by PKULaw and Wind. After a remote Registry refresh, Yingmi, QVeris, Bazhuayu Cloud Collection, GitHub, Cloudflare, Notion, Tavily, and the Ace Data Cloud-hosted Seedream connector are also available, for 14 published marketplace cards after deduplication. Recommendations are limited to the four Qichacha cards, PKULaw, and Wind; all other connectors remain discoverable in their business categories.
30
30
 
31
31
  ## Interface and demo
32
32
 
@@ -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.11`](https://www.npmjs.com/package/dsh-mcp-connector), with [GitHub Release v0.2.11](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.11).
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).
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
@@ -15,7 +15,7 @@
15
15
  ## 功能
16
16
 
17
17
  - 左侧主导航入口:目标位置为“新会话”下方、“工作区/会话列表”上方;若 DSH DOM 结构不兼容,自动回退到底部公开插槽。
18
- - 图形化市场:市场/已安装、推荐与 9 类业务分类、全文搜索、服务商/接入方式组合筛选、刷新与主动健康检查;区分“已配置”、“已连接”、“需重新授权”、“部分异常”和“连接异常”。
18
+ - 图形化市场:默认“全部”按推荐与 9 类业务分类分章节展示,每章先展示 4 张并可展开;分类栏固定可见,单分类页展示全部卡片。
19
19
  - 图形化添加:手动 HTTP/stdio、`mcpServers` JSON、连接器描述 URL 三种入口,失败时保留表单并给出修复建议。
20
20
  - 连接器详情:精选 Prompt 优先展示,点击可带入 DSH 新会话;工具按 Server 分组,支持描述、搜索和独立滚动。
21
21
  - Prompt 模板:使用 `{{company}}` 等变量,发送前填写真实查询主体。
@@ -28,7 +28,7 @@
28
28
  - 平滑迁移:显式扫描并复制两个旧企查查 OAuth 插件授权;不删除原插件或原凭据。
29
29
  - 对话工具:`mcp_connector_catalog`、`connect`、`configure`、`import_json`、`install_from_url`、`status`、`health_check`、`set_enabled`、`disconnect`、`refresh_catalog`、`publish`、`tools_list`。
30
30
 
31
- 随包目录前 4 张为企查查连接器,另含“北大法宝·法律检索”和“Wind·股票数据”。远程 Registry 刷新后还会加入“盈米·基金投顾”、“QVerisMCP”和“八爪鱼·云采集”,当前共 9 张已发布市场卡片;其中八爪鱼是首个完整跑通标准 OAuth 2.1 + PKCE 动态注册链路的第三方示例,插件架构不限定厂商。
31
+ 随包目录前 4 张为企查查连接器,另含“北大法宝·法律检索”和“Wind·股票数据”。远程 Registry 刷新后还会加入“盈米·基金投顾”、“QVerisMCP”、“八爪鱼·云采集”、GitHub、Cloudflare、Notion、Tavily 和由 Ace Data Cloud 托管的 Seedream,合并去重后当前共 14 张已发布市场卡片。推荐位严格保留 4 张企查查卡片、北大法宝和 Wind;其他连接器按业务分类展示。
32
32
 
33
33
  ## 界面与演示
34
34
 
@@ -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.11`](https://www.npmjs.com/package/dsh-mcp-connector),对应 [GitHub Release v0.2.11](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.11)。
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)。
100
100
 
101
101
  版本能力与变更记录见 [CHANGELOG.md](CHANGELOG.md)。
102
102
  Desktop 发版回归见 [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md)。
@@ -67,12 +67,12 @@
67
67
  "name": "企查查·法律数据",
68
68
  "vendor": "企查查 Qichacha",
69
69
  "icon": "/mcp-connector/ui/assets/qcc-logo.svg",
70
- "category": "企业数据",
70
+ "category": "法律合规",
71
71
  "summary": "法规条文 / 司法案例检索与核验",
72
72
  "description": "法律法规逐字条文、指导性/公报/典型案例检索、案号引用核验,面向法律意见书与类案检索场景。",
73
73
  "tags": ["法律", "法规", "案例", "核验"],
74
74
  "published": true,
75
- "featured": false,
75
+ "featured": true,
76
76
  "homepage": "https://open.qcc.com",
77
77
  "promptVariables": [
78
78
  { "name": "topic", "label": "法律主题", "default": "劳动合同解除", "placeholder": "例如:劳动合同解除", "required": true }
@@ -106,7 +106,7 @@
106
106
  "description": "覆盖全国招投标公告、中标结果、采购信息,适用于竞对分析、市场研究、销售线索挖掘场景。",
107
107
  "tags": ["招投标", "采购", "中标", "竞对"],
108
108
  "published": true,
109
- "featured": false,
109
+ "featured": true,
110
110
  "homepage": "https://open.qcc.com",
111
111
  "promptVariables": [
112
112
  { "name": "keyword", "label": "项目关键词", "default": "智慧城市", "placeholder": "例如:智慧城市", "required": true },
@@ -134,12 +134,12 @@
134
134
  "name": "企查查·文档报告",
135
135
  "vendor": "企查查 Qichacha",
136
136
  "icon": "/mcp-connector/ui/assets/qcc-logo.svg",
137
- "category": "企业数据",
137
+ "category": "效率工具",
138
138
  "summary": "企业报告生成 / 文档分析",
139
139
  "description": "基于企业数据自动生成分析报告、尽职调查文档,支持文档结构化解析与内容提取。",
140
140
  "tags": ["文档", "报告", "分析"],
141
141
  "published": true,
142
- "featured": false,
142
+ "featured": true,
143
143
  "homepage": "https://open.qcc.com",
144
144
  "promptVariables": [
145
145
  { "name": "company", "label": "企业名称", "default": "小米科技有限责任公司", "placeholder": "例如:小米科技有限责任公司", "required": true }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-mcp-connector",
3
- "version": "0.2.12",
3
+ "version": "0.2.14",
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
@@ -105,16 +105,12 @@
105
105
  .stats .count { font-size: 13px; color: var(--text-2); }
106
106
  .stats .count strong { color: var(--text); font-weight: 600; }
107
107
  .market-filters {
108
- display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
109
- margin-bottom: 14px; padding: 10px 12px;
108
+ position: sticky; top: 0; z-index: 4;
109
+ display: flex; align-items: center;
110
+ margin: -8px 0 14px; padding: 10px 12px;
110
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);
111
113
  }
112
- .market-filters .filter-label { color: var(--muted); font-size: 12px; flex: 0 0 auto; }
113
- .market-filters select {
114
- min-width: 150px; height: 34px; border: 1px solid var(--line); border-radius: 8px;
115
- background: var(--bg); color: var(--text); padding: 0 30px 0 10px; font: inherit; font-size: 12px;
116
- }
117
- .market-filters select:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-light); }
118
114
  .filter-chips { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
119
115
  .filter-chip {
120
116
  border: 1px solid var(--line); background: var(--bg); color: var(--text-2);
@@ -122,9 +118,17 @@
122
118
  }
123
119
  .filter-chip:hover { border-color: var(--accent); color: var(--accent); }
124
120
  .filter-chip.active { border-color: var(--accent); background: var(--accent-light); color: var(--accent); font-weight: 600; }
125
- .filter-reset { margin-left: auto; }
126
- .category-chips { display: flex; gap: 6px; flex-wrap: nowrap; overflow-x: auto; padding-bottom: 4px; -webkit-overflow-scrolling: touch; scrollbar-width: thin; }
127
- .category-chip { flex-shrink: 0; }
121
+ .category-chips { width: 100%; display: flex; gap: 5px; flex-wrap: nowrap; overflow: visible; }
122
+ .category-chip { flex: 0 0 auto; padding-inline: 9px; }
123
+
124
+ /* Grouped marketplace */
125
+ .market-section { margin-bottom: 26px; }
126
+ .market-section:last-child { margin-bottom: 0; }
127
+ .market-section-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 10px; }
128
+ .market-section-title { display: flex; align-items: baseline; gap: 8px; margin: 0; font-size: 14px; font-weight: 650; }
129
+ .market-section-count { color: var(--muted); font-size: 11px; font-weight: 400; }
130
+ .market-section-toggle { border: 0; background: transparent; color: var(--text-2); cursor: pointer; font: inherit; font-size: 12px; padding: 4px 0; }
131
+ .market-section-toggle:hover { color: var(--accent); }
128
132
 
129
133
  /* Grid - TraeWork 风格:3 列 */
130
134
  .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 12px; }
@@ -370,8 +374,7 @@
370
374
  .card .actions { width: 100%; justify-content: flex-end; }
371
375
  .form-row { grid-template-columns: 1fr; gap: 0; }
372
376
  .market-filters { align-items: stretch; }
373
- .market-filters select { width: 100%; }
374
- .filter-reset { margin-left: 0; }
377
+ .category-chips { flex-wrap: wrap; }
375
378
  }
376
379
 
377
380
  /* Scrollbar */
@@ -478,8 +481,6 @@
478
481
  let view = 'market';
479
482
  let marketItems = {};
480
483
  let searchQuery = '';
481
- let marketVendor = '';
482
- let marketAuth = '';
483
484
  let marketCategory = '';
484
485
  const PROMPT_PAGE_SIZE = 4;
485
486
  let detailPrompts = [];
@@ -490,7 +491,8 @@
490
491
  let detailFailureState = '';
491
492
  let toolRenderLimit = 50;
492
493
  let detailLoadId = 0;
493
- let catalogRenderLimit = 60;
494
+ const MARKET_SECTION_PREVIEW_LIMIT = 4;
495
+ const expandedMarketSections = new Set();
494
496
  let lastModalFocus = null;
495
497
  let pendingPrompt = null;
496
498
  const PROMPT_REQUEST_TYPE = 'mcp-connector:start-session';
@@ -706,13 +708,6 @@
706
708
  || (d.tags || []).some(t => t.toLowerCase().includes(q));
707
709
  }
708
710
 
709
- const AUTH_FILTERS = [
710
- { value: '', label: '全部接入' },
711
- { value: 'oauth2-pkce', label: 'OAuth' },
712
- { value: 'credential', label: 'Key / Token' },
713
- { value: 'none', label: '免密' },
714
- ];
715
-
716
711
  const CATEGORIES = [
717
712
  { value: '', label: '全部' },
718
713
  { value: 'recommended', label: '推荐' },
@@ -724,6 +719,7 @@
724
719
  { value: '调研分析', label: '调研分析' },
725
720
  { value: '设计创意', label: '设计创意' },
726
721
  { value: '效率工具', label: '效率工具' },
722
+ { value: '其他', label: '其他' },
727
723
  ];
728
724
 
729
725
  const CATEGORY_MAP = {
@@ -738,23 +734,39 @@
738
734
  if (!matchesSearch(d)) return false;
739
735
  if (marketCategory === 'recommended' && !d.featured) return false;
740
736
  if (marketCategory && marketCategory !== 'recommended' && normalizeCategory(d.category) !== marketCategory) return false;
741
- if (marketVendor && d.vendor !== marketVendor) return false;
742
- if (marketAuth === 'credential') return ['bearer', 'api-key'].includes(d.authMode);
743
- if (marketAuth && d.authMode !== marketAuth) return false;
744
737
  return true;
745
738
  }
746
739
 
747
- function marketFilterHtml(items) {
740
+ function marketFilterHtml() {
748
741
  const categoryChips = CATEGORIES.map((cat) => `<button class="filter-chip category-chip ${cat.value === marketCategory ? 'active' : ''}" type="button" data-category-filter="${esc(cat.value)}" aria-pressed="${cat.value === marketCategory ? 'true' : 'false'}">${esc(cat.label)}</button>`).join('');
749
- const vendors = [...new Set(items.map((item) => item.vendor).filter(Boolean))]
750
- .sort((a, b) => a.localeCompare(b, 'zh-CN'));
751
- const vendorOptions = [`<option value="">全部服务商</option>`, ...vendors.map((vendor) => `<option value="${esc(vendor)}" ${vendor === marketVendor ? 'selected' : ''}>${esc(vendor)}</option>`)].join('');
752
- const authButtons = AUTH_FILTERS.map((filter) => `<button class="filter-chip ${filter.value === marketAuth ? 'active' : ''}" type="button" data-auth-filter="${esc(filter.value)}" aria-pressed="${filter.value === marketAuth ? 'true' : 'false'}">${esc(filter.label)}</button>`).join('');
753
- const active = Boolean(marketVendor || marketAuth || marketCategory);
754
- return `<div class="market-filters" role="group" aria-label="市场筛选"><div class="filter-chips category-chips" role="group" aria-label="按分类筛选">${categoryChips}</div><span class="filter-label">服务商</span><select id="market-vendor-filter" aria-label="按服务商筛选">${vendorOptions}</select><span class="filter-label">接入方式</span><div class="filter-chips" role="group" aria-label="按接入方式筛选">${authButtons}</div><button class="text-btn filter-reset" id="market-filter-reset" type="button" ${active ? '' : 'hidden'}>清除筛选</button></div>`;
755
- }
756
-
757
- async function loadMarket({ checkHealth = true } = {}) {
742
+ return `<div class="market-filters" role="group" aria-label="市场分类"><div class="filter-chips category-chips" role="group" aria-label="按分类筛选">${categoryChips}</div></div>`;
743
+ }
744
+
745
+ function marketSectionHtml(value, label, items) {
746
+ if (!items.length) return '';
747
+ const expanded = expandedMarketSections.has(value);
748
+ const visible = expanded ? items : items.slice(0, MARKET_SECTION_PREVIEW_LIMIT);
749
+ const toggle = items.length > MARKET_SECTION_PREVIEW_LIMIT
750
+ ? `<button class="market-section-toggle" type="button" data-section-toggle="${esc(value)}" aria-expanded="${expanded ? 'true' : 'false'}">${expanded ? '收起' : `查看全部 ${items.length} 个`}</button>`
751
+ : '';
752
+ return `<section class="market-section" data-market-section="${esc(value)}"><div class="market-section-head"><h2 class="market-section-title">${esc(label)}<span class="market-section-count">${items.length} 个</span></h2>${toggle}</div><div class="grid">${visible.map(cardHtml).join('')}</div></section>`;
753
+ }
754
+
755
+ function groupedMarketHtml(items) {
756
+ const searchable = items.filter(matchesSearch);
757
+ const sections = [
758
+ { value: 'recommended', label: '推荐', items: searchable.filter((item) => item.featured) },
759
+ ...CATEGORIES.filter((category) => category.value && category.value !== 'recommended').map((category) => ({
760
+ ...category,
761
+ items: searchable.filter((item) => normalizeCategory(item.category) === category.value),
762
+ })),
763
+ ];
764
+ return sections.map((section) => marketSectionHtml(section.value, section.label, section.items)).join('')
765
+ || '<div class="empty">没有匹配当前搜索条件的连接器</div>';
766
+ }
767
+
768
+ async function loadMarket({ checkHealth = true, preserveScroll = false } = {}) {
769
+ const previousScrollTop = preserveScroll ? main.scrollTop : 0;
758
770
  main.innerHTML = '<div class="empty">加载中…</div>';
759
771
  const r = await call('catalog', {});
760
772
  if (!r.ok) { main.innerHTML = `<div class="empty">${esc(r.message)}</div>`; return; }
@@ -763,15 +775,16 @@
763
775
  marketItems = {};
764
776
  items.forEach((d) => { marketItems[d.id] = d; });
765
777
  const filtered = items.filter(matchesMarketFilters);
766
- const count = filtered.length;
767
- const visible = filtered.slice(0, catalogRenderLimit);
768
- const authLabel = AUTH_FILTERS.find((filter) => filter.value === marketAuth)?.label;
778
+ const count = marketCategory ? filtered.length : items.filter(matchesSearch).length;
769
779
  const categoryLabel = CATEGORIES.find((cat) => cat.value === marketCategory)?.label;
770
- const filterSummary = `${marketCategory ? ` · 分类「${esc(categoryLabel)}」` : ''}${marketVendor ? ` · 服务商「${esc(marketVendor)}」` : ''}${marketAuth ? ` · 接入方式「${esc(authLabel)}」` : ''}`;
771
- const resultHtml = visible.length > 0
772
- ? `<div class="grid">${visible.map(cardHtml).join('')}</div>${visible.length < count ? `<button class="tools-more" id="catalog-more" type="button">再显示 ${Math.min(60, count - visible.length)} 个(剩余 ${count - visible.length})</button>` : ''}`
773
- : '<div class="empty">没有匹配当前筛选条件的连接器</div>';
774
- main.innerHTML = `${marketFilterHtml(items)}<div class="stats"><span class="count">共 <strong>${count}</strong> 个连接器${searchQuery ? ` · 搜索「${esc(searchQuery)}」` : ''}${filterSummary} · 点击卡片查看能力,点击「连接」完成授权</span></div>${resultHtml}`;
780
+ const filterSummary = marketCategory ? ` · 分类「${esc(categoryLabel)}」` : '';
781
+ const resultHtml = !marketCategory
782
+ ? groupedMarketHtml(items)
783
+ : filtered.length > 0
784
+ ? `<div class="grid">${filtered.map(cardHtml).join('')}</div>`
785
+ : '<div class="empty">没有匹配当前筛选条件的连接器</div>';
786
+ main.innerHTML = `${marketFilterHtml()}<div class="stats"><span class="count">共 <strong>${count}</strong> 个连接器${searchQuery ? ` · 搜索「${esc(searchQuery)}」` : ''}${filterSummary} · 点击卡片查看能力,点击「连接」完成授权</span></div>${resultHtml}`;
787
+ if (preserveScroll) main.scrollTop = previousScrollTop;
775
788
  if (checkHealth && !initialHealthCheckStarted && items.some((item) => item.connected?.length > 0)) {
776
789
  initialHealthCheckStarted = true;
777
790
  call('healthCheck', {}).then(() => {
@@ -1323,37 +1336,17 @@
1323
1336
  const categoryFilter = event.target.closest('[data-category-filter]');
1324
1337
  if (categoryFilter) {
1325
1338
  marketCategory = categoryFilter.dataset.categoryFilter || '';
1326
- catalogRenderLimit = 60;
1327
- loadMarket({ checkHealth: false });
1328
- return;
1329
- }
1330
- const authFilter = event.target.closest('[data-auth-filter]');
1331
- if (authFilter) {
1332
- marketAuth = authFilter.dataset.authFilter || '';
1333
- catalogRenderLimit = 60;
1334
1339
  loadMarket({ checkHealth: false });
1335
1340
  return;
1336
1341
  }
1337
- if (event.target.closest('#market-filter-reset')) {
1338
- marketVendor = '';
1339
- marketAuth = '';
1340
- marketCategory = '';
1341
- catalogRenderLimit = 60;
1342
- loadMarket({ checkHealth: false });
1343
- return;
1342
+ const sectionToggle = event.target.closest('[data-section-toggle]');
1343
+ if (sectionToggle) {
1344
+ const section = sectionToggle.dataset.sectionToggle;
1345
+ if (expandedMarketSections.has(section)) expandedMarketSections.delete(section);
1346
+ else expandedMarketSections.add(section);
1347
+ loadMarket({ checkHealth: false, preserveScroll: true });
1344
1348
  }
1345
- if (event.target.closest('#catalog-more')) {
1346
- catalogRenderLimit += 60;
1347
- loadMarket({ checkHealth: false });
1348
- }
1349
- });
1350
- main.addEventListener('change', (event) => {
1351
- if (event.target.id !== 'market-vendor-filter') return;
1352
- marketVendor = event.target.value;
1353
- catalogRenderLimit = 60;
1354
- loadMarket({ checkHealth: false });
1355
1349
  });
1356
-
1357
1350
  $('#refresh').addEventListener('click', async () => {
1358
1351
  const r = await call('refreshCatalog', {});
1359
1352
  showToast(r.message, r.ok);
@@ -1374,7 +1367,7 @@
1374
1367
 
1375
1368
  searchInput.addEventListener('input', (e) => {
1376
1369
  searchQuery = e.target.value.trim();
1377
- catalogRenderLimit = 60;
1370
+ expandedMarketSections.clear();
1378
1371
  refresh();
1379
1372
  });
1380
1373