dsh-mcp-connector 0.2.71 → 0.2.73
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 +15 -0
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/docs/DESKTOP-E2E.md +7 -0
- package/lib/client.js +3 -3
- package/lib/index.js +6 -11
- package/lib/web.js +38 -4
- package/package.json +1 -1
- package/ui/index.html +93 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
本项目遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/);首个公开版本从 `0.1.0` 开始。
|
|
4
4
|
|
|
5
|
+
## [0.2.73] - 2026-10-15
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- 兼容 DSH 0.2.1-alpha.1 起的 webStartup 服务,同时保留旧宿主 webRuntime 支持,修复面板正文及 API 路由未挂载导致的空白(#151)。两种服务并存时优先使用 webStartup 的可信主机配置,服务卸载时清理路由。
|
|
10
|
+
- 将插件 API 请求失败与 npm 版本查询失败区分提示,不再把路由不可用统一报告为 npm 网络错误。
|
|
11
|
+
|
|
12
|
+
## [0.2.72] - 2026-10-14
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- 宿主切换面板销毁 iframe 后,按工作区恢复页签、搜索、市场分类、展开章节、工具筛选/分页及列表滚动。状态仅保留在同源宿主当前页面内存,最多 32 个工作区,不保存凭据表单、API 返回或授权弹框;刷新宿主页面即清除。
|
|
17
|
+
- 市场与已安装列表加入请求序列和工作区检查,防止较早响应覆盖新页签/工作区;加载中的滚动归零不覆盖恢复位置。
|
|
18
|
+
- 本次为浏览状态恢复而非 iframe 保活:仍重新拉取数据、重建 SSE,详情和未完成表单不恢复。
|
|
19
|
+
|
|
5
20
|
## [0.2.71] - 2026-10-14
|
|
6
21
|
|
|
7
22
|
### 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.
|
|
218
|
+
The current public version is [`dsh-mcp-connector@0.2.73`](https://www.npmjs.com/package/dsh-mcp-connector), with [GitHub Release v0.2.73](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.73).
|
|
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.
|
|
213
|
+
当前公开版本为 [`dsh-mcp-connector@0.2.73`](https://www.npmjs.com/package/dsh-mcp-connector),对应 [GitHub Release v0.2.73](https://github.com/duhu2000/dsh-mcp-connector/releases/tag/v0.2.73)。
|
|
214
214
|
|
|
215
215
|
版本能力与变更记录见 [CHANGELOG.md](CHANGELOG.md)。
|
|
216
216
|
Desktop 发版回归见 [docs/DESKTOP-E2E.md](docs/DESKTOP-E2E.md)。
|
package/docs/DESKTOP-E2E.md
CHANGED
|
@@ -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/lib/client.js
CHANGED
|
@@ -1193,7 +1193,7 @@ window.__ModuleLoader__.load({
|
|
|
1193
1193
|
if (disposed) return;
|
|
1194
1194
|
setVersionStatus(status);
|
|
1195
1195
|
setVersionCheckBusy(status.checking === true);
|
|
1196
|
-
setVersionCheckFailed(!status.checking && status.sources?.npm?.ok !== true);
|
|
1196
|
+
setVersionCheckFailed(!status.checking && status.sources?.npm?.ok !== true ? "npm" : false);
|
|
1197
1197
|
if (status.checking) {
|
|
1198
1198
|
schedule(1e3);
|
|
1199
1199
|
return;
|
|
@@ -1207,7 +1207,7 @@ window.__ModuleLoader__.load({
|
|
|
1207
1207
|
console.warn("[mcp-connector] plugin update check failed:", error);
|
|
1208
1208
|
if (!disposed) {
|
|
1209
1209
|
setVersionCheckBusy(false);
|
|
1210
|
-
setVersionCheckFailed(
|
|
1210
|
+
setVersionCheckFailed("api");
|
|
1211
1211
|
schedule(VERSION_CHECK_RETRY_MS);
|
|
1212
1212
|
}
|
|
1213
1213
|
});
|
|
@@ -1668,7 +1668,7 @@ window.__ModuleLoader__.load({
|
|
|
1668
1668
|
versionCheckFailed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1669
1669
|
className: "mcpConnectorUpdateStatus",
|
|
1670
1670
|
"data-tone": "error",
|
|
1671
|
-
children: "暂时无法确认 npm 最新版本,请重试"
|
|
1671
|
+
children: versionCheckFailed === "api" ? "插件 API 不可用,请检查 DSH 宿主兼容性或服务日志" : "暂时无法确认 npm 最新版本,请重试"
|
|
1672
1672
|
}),
|
|
1673
1673
|
versionStatus?.releasePending && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1674
1674
|
className: "mcpConnectorVersion",
|
package/lib/index.js
CHANGED
|
@@ -40,7 +40,7 @@ import { provision, disable, remove, entryIdFor } from './mcp-provision.js';
|
|
|
40
40
|
import { classifyConnectionError, validateConnectionRecords } from './mcp-validation.js';
|
|
41
41
|
import { listMcpTools, McpHttpError } from './mcp-http.js';
|
|
42
42
|
import { registerTools } from './tools.js';
|
|
43
|
-
import {
|
|
43
|
+
import { injectWebRoutes } from './web.js';
|
|
44
44
|
import { createVersionStatusService } from './version-status.js';
|
|
45
45
|
import { assertSafeUrl, normalizeConnectionDisplayName } from './util.js';
|
|
46
46
|
import { readLegacyGrantCandidates, planLegacyMigration, toConnectorGrant, toConnectionRecords } from './migration.js';
|
|
@@ -3445,17 +3445,13 @@ export async function apply(ctx, config) {
|
|
|
3445
3445
|
const disposeTools = registerTools(ctx, observableApi);
|
|
3446
3446
|
|
|
3447
3447
|
/* ───────────────────────── web 半区(图形化市场)──────────────────────── */
|
|
3448
|
-
// webServer
|
|
3449
|
-
let disposeWeb = null;
|
|
3448
|
+
// webServer + 新版 webStartup 或旧版 webRuntime;纯对话部署不挂载 HTTP 路由。
|
|
3450
3449
|
if (typeof ctx.inject === 'function') {
|
|
3451
3450
|
try {
|
|
3452
|
-
ctx
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
frameAncestors: config.frameAncestors,
|
|
3457
|
-
});
|
|
3458
|
-
logger.info('mcp-connector web UI mounted at /mcp-connector/ui/');
|
|
3451
|
+
injectWebRoutes(ctx, observableApi, {
|
|
3452
|
+
logger,
|
|
3453
|
+
eventHub: statusEvents,
|
|
3454
|
+
frameAncestors: config.frameAncestors,
|
|
3459
3455
|
});
|
|
3460
3456
|
} catch (error) {
|
|
3461
3457
|
logger.warn(`mcp-connector web UI not mounted (webServer unavailable): ${error.message}`);
|
|
@@ -3467,7 +3463,6 @@ export async function apply(ctx, config) {
|
|
|
3467
3463
|
clearTimeout(discoveryTimer);
|
|
3468
3464
|
clearInterval(discoveryInterval);
|
|
3469
3465
|
disposeTools();
|
|
3470
|
-
disposeWeb?.();
|
|
3471
3466
|
hostGovernance.dispose();
|
|
3472
3467
|
hostScopes.dispose();
|
|
3473
3468
|
hostSessionInjection.dispose();
|
package/lib/web.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - GET /mcp-connector/events 无敏感数据的实时状态 SSE
|
|
8
8
|
*
|
|
9
9
|
* 所有路由都过浏览器信任 fence(loopback + 同源),与 better-sidebar 同一策略。
|
|
10
|
-
*
|
|
10
|
+
* 本模块依赖 ctx.webServer 及 webStartup(新版)或 webRuntime(旧版);非 web 部署时
|
|
11
11
|
* 调用方应跳过挂载,保证插件仍可用于纯对话工具场景。
|
|
12
12
|
*
|
|
13
13
|
* 静态资源响应的 `Content-Security-Policy` 由 `buildCsp()` 生成;其中
|
|
@@ -264,13 +264,47 @@ export function buildCsp(frameAncestors) {
|
|
|
264
264
|
|
|
265
265
|
/* ───────────────────────── 路由挂载 ───────────────────────── */
|
|
266
266
|
|
|
267
|
+
/** Alternative dependencies, not an AND: alpha.1 removed webRuntime.
|
|
268
|
+
* Keep each mount owned by its injection context; prefer webStartup when both exist.
|
|
269
|
+
*/
|
|
270
|
+
export function injectWebRoutes(ctx, api, options = {}) {
|
|
271
|
+
const candidates = new Map();
|
|
272
|
+
let active;
|
|
273
|
+
let dispose;
|
|
274
|
+
const reconcile = () => {
|
|
275
|
+
const next = candidates.get('webStartup') ?? candidates.get('webRuntime');
|
|
276
|
+
if (next === active) return;
|
|
277
|
+
dispose?.();
|
|
278
|
+
dispose = undefined;
|
|
279
|
+
active = next;
|
|
280
|
+
if (next) {
|
|
281
|
+
dispose = mountWebRoutes(next.ctx, api, {
|
|
282
|
+
...options, trustedHosts: next.trustedHosts,
|
|
283
|
+
});
|
|
284
|
+
options.logger?.info?.('mcp-connector web UI mounted at /mcp-connector/ui/');
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
for (const name of ['webStartup', 'webRuntime']) {
|
|
288
|
+
ctx.inject(['webServer', name], (wctx) => {
|
|
289
|
+
wctx.effect(() => {
|
|
290
|
+
const candidate = { ctx: wctx, trustedHosts: wctx[name].trustedHosts ?? [] };
|
|
291
|
+
candidates.set(name, candidate);
|
|
292
|
+
reconcile();
|
|
293
|
+
return () => {
|
|
294
|
+
if (candidates.get(name) === candidate) candidates.delete(name);
|
|
295
|
+
reconcile();
|
|
296
|
+
};
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
267
302
|
/**
|
|
268
|
-
* 挂载 web 路由。`wctx` 为已注入 webServer
|
|
303
|
+
* 挂载 web 路由。`wctx` 为已注入 webServer 和对应信任服务的子上下文。
|
|
269
304
|
* `frameAncestors` 为面板可嵌入白名单(缺省 = `['self']`,只有同源页面能内嵌;
|
|
270
305
|
* 桌面壳场景由调用方显式追加外壳源)。返回 disposer(卸载三条 prefix 路由)。
|
|
271
306
|
*/
|
|
272
|
-
export function mountWebRoutes(wctx, api, { logger, eventHub, frameAncestors } = {}) {
|
|
273
|
-
const trustedHosts = wctx.webRuntime?.trustedHosts ?? [];
|
|
307
|
+
export function mountWebRoutes(wctx, api, { logger, eventHub, frameAncestors, trustedHosts = wctx.webStartup?.trustedHosts ?? wctx.webRuntime?.trustedHosts ?? [] } = {}) {
|
|
274
308
|
const fence = (req) => isTrustedWebRequest(req, trustedHosts);
|
|
275
309
|
const csp = buildCsp(frameAncestors);
|
|
276
310
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mcp-connector",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.73",
|
|
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
|
-
|
|
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();
|