dsh-armory 2.4.2 → 2.4.4

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/lib/index.js CHANGED
@@ -5,7 +5,7 @@ import { homedir } from "node:os";
5
5
  import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
6
6
  import { interpolate } from "@deepseek-ai/cordis-plugin-loader";
7
7
  import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
8
- import { basename, dirname, join } from "node:path";
8
+ import { basename, dirname, isAbsolute, join } from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import { normalizeMcpImport, readManagedMcpServers, setManagedMcpDisabled, updateManagedMcpPatch } from "./mcp-config.js";
11
11
  import { admitImportedServers } from "./import-admission.js";
@@ -14,15 +14,22 @@ import { deriveMcpPhase, formatMcpLog, sanitizeMcpLog } from "./mcp-observabilit
14
14
  import { allMcpEntries, entriesForName, projectToolSchema, revisionOf, toolInventory } from "./mcp-registry.js";
15
15
  // Skill 看板的卸载落盘点:来源白名单 + 根目录包含性 + 符号链接/根目录校验(lib/skill-remove.js)。
16
16
  import { removeSkillFromDisk } from "./skill-remove.js";
17
+ // 工具裁缝技能账本的禁用/释放落盘点:写 SKILL.md frontmatter 的 disable-model-invocation
18
+ // 标志(宿主 dsh-skill-filesystem 官方键),安全闸门与 skill-remove 同一套(lib/skill-disable.js)。
19
+ import { setSkillModelInvocationOnDisk } from "./skill-disable.js";
17
20
  // Skill 看板的安装时间 + 市场原名投影(名字自建映射,lib/skill-dir-times.js)。
18
21
  import { skillInstallInfo } from "./skill-dir-times.js";
22
+ // 系统级打开(issue #7):headless 主机没有 xdg-open,spawn 的 ENOENT 必须有监听者,
23
+ // 否则异步 error 事件会带走整个宿主进程(lib/system-open.js)。
24
+ import { isAllowedExternalUrl, openSystemUrl, revealSystemPath } from "./system-open.js";
19
25
  import { WHATS_NEW } from "./whats-new.js";
20
26
  import { attachWhatsNewEnglish } from "./whats-new-en.js";
21
- import { probeMcpServersAuthorization, wbConnectorCatalog, wbConnectorCachedCount, wbConnectorIcon, wbConnectorPreview, wbSkillCatalog, wbSkillDetail, wbSkillInstall, wbSkillInstalled, wbSkillUninstall } from "./wb-market.js";
22
27
  // T03/R4:多市场注册表(连接器源)的宿主侧数据层。
23
28
  import { readRegistry, upsertMarketplace } from "./wb-registry.js";
24
29
  // T06/Awesome:DSH 社区插件市场(awesome-dsh-plugins)的宿主侧数据层。
25
30
  import { wbAwesomeCatalog, wbAwesomeDetail, wbAwesomeReadme, wbAwesomeUpdateCheck } from "./wb-awesome-market.js";
31
+ // 社区 MCP:官方 MCP Registry(registry.modelcontextprotocol.io)只读取数,数据层在 lib/mcp-community.js。
32
+ import { wbMcpCommunityList } from "./mcp-community.js";
26
33
  // 社区元信息(维护者 / 最近提交 / issue / star):打开详情时按需查,6 小时缓存。
27
34
  import { wbRepoMeta } from "./community-meta.js";
28
35
  // npm 近 30 天下载量:插件卡片底栏的「↓N」,目录里没有这个字段,只能去 npm 统计接口取。
@@ -45,13 +52,34 @@ import {
45
52
  saveManualToken as oauthSaveManualToken,
46
53
  submitCallback as oauthSubmitCallback,
47
54
  } from "./mcp-oauth.js";
48
- import { hasTeamBridge, installExpertSkills, listExpertRecent, listExpertSkills, prepareExpertPersona, readExpertDisplayNames, readExpertPresets, recordExpertRecent, removeExpertPreset, uninstallExpertSkills, uninstallTeamMemberPersonas, wbExpertCatalog, wbExpertDetail, writeExpertPreset } from "./expert-market.js";
49
- import { attachPersona, detachPersona, listPersonas, reconcilePersonas } from "./session-persona.js";
55
+ import { attachPersona, detachPersona, installPersonaRuntime, listPersonas, reconcilePersonas } from "./session-persona.js";
50
56
  // clearTextCache 的 import 已随 toolTailor 的「清空缓存」动作一起删掉(见下方 toolTailor 注释)。
51
57
  import { scanSessionLedger } from "./tool-ledger.js";
52
58
  import { evaluateEnvExpression, jsExpressionToTemplate } from "./workspace-config.js";
53
59
  import { canonicalWorkspacePath, installAgentRuntime, listWorkspaceRecords, liveWorkspaceAgentCount, projectConnectionsView, readWorkspaceConfigCached, reconcileWorkspaceConnections, reconcileWorkspaceRestricts, reconnectWorkspaceServer, withWorkspaceWrite, workspaceConnectionStatus, workspaceMountErrorsView, workspaceRestrictErrorView, workspaceScopeErrorsView, workspaceToolSchemas, writeWorkspaceConfigFile } from "./workspace-runtime.js";
54
60
 
61
+ // ── P0-1:市场数据模块按需加载 ──────────────────────────────────────────
62
+ // wb-market(连接器 ZIP + SkillHub)与 expert-market(专家目录)合计约 68ms 的冷导入,
63
+ // 面板一次都没打开时纯属宿主启动成本。两者只被市场 / 专家相关 RPC 用到,因此改为
64
+ // 首次调用时才 import。缓存的是 Promise 本身:并发首次调用共享同一次解析,不会重复加载;
65
+ // 解析失败时清掉缓存,下次调用可重试(不会永久卡在 rejected Promise 上)。
66
+ let marketModule = null;
67
+ let expertModule = null;
68
+ const loadMarket = () => {
69
+ marketModule ??= import("./wb-market.js").catch((error) => {
70
+ marketModule = null;
71
+ throw error;
72
+ });
73
+ return marketModule;
74
+ };
75
+ const loadExpert = () => {
76
+ expertModule ??= import("./expert-market.js").catch((error) => {
77
+ expertModule = null;
78
+ throw error;
79
+ });
80
+ return expertModule;
81
+ };
82
+
55
83
  var __runInitializers = function(thisArg, initializers, value) {
56
84
  var useValue = arguments.length > 2;
57
85
  for (var i = 0; i < initializers.length; i++) value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
@@ -180,7 +208,8 @@ async function injectStoredOAuthTokens(mcpServers) {
180
208
 
181
209
  /** 解析某连接器里某 server 的授权靶点(url + 现有 headers)。 */
182
210
  async function resolveOAuthTarget(connectorId, serverName) {
183
- const preview = await wbConnectorPreview(connectorId);
211
+ const marketApi = await loadMarket();
212
+ const preview = await marketApi.wbConnectorPreview(connectorId);
184
213
  const servers = preview?.mcpServers && typeof preview.mcpServers === "object" ? preview.mcpServers : {};
185
214
  const name = String(serverName || "").trim() || Object.keys(servers)[0] || "";
186
215
  const rawServer = servers[name];
@@ -215,6 +244,9 @@ function resolveShellService(ctx) {
215
244
  async function openExternalUrl(ctx, url) {
216
245
  const target = String(url || "").trim();
217
246
  if (!target) return { opened: false, reason: "empty-url" };
247
+ // 协议白名单在交给宿主 shell 之前先过一遍:Electron 的 openExternal 同样能打开
248
+ // file: / 本地可执行文件,不能因为「宿主有 shell」就跳过校验。
249
+ if (!isAllowedExternalUrl(target)) return { opened: false, reason: "blocked-url" };
218
250
  const shell = resolveShellService(ctx);
219
251
  if (shell && typeof shell.openExternal === "function") {
220
252
  try {
@@ -225,17 +257,10 @@ async function openExternalUrl(ctx, url) {
225
257
  }
226
258
  }
227
259
  try {
228
- const { spawn } = await import("node:child_process");
229
- if (process.platform === "win32") {
230
- // 绝不能用 `cmd /c start "$url"`:Node 只在参数含空格时才给参数加引号,而 URL 不含空格,
231
- // cmd 于是把 URL 里的 & 当成命令分隔符,授权地址被截断成 `?response_type=code`,
232
- // 服务器随后回 `invalid_request: client_id is required`。
233
- // rundll32 走 ShellExecute 且参数直传,不经 cmd 解析。
234
- spawn("rundll32", ["url.dll,FileProtocolHandler", target], { detached: true, stdio: "ignore" }).unref();
235
- } else {
236
- spawn(process.platform === "darwin" ? "open" : "xdg-open", [target], { detached: true, stdio: "ignore" }).unref();
237
- }
238
- return { opened: true, via: "spawn" };
260
+ const opened = await openSystemUrl(target, { logger: ctx?.logger });
261
+ if (opened.ok) return { opened: true, via: "spawn" };
262
+ // headless 主机没有桌面打开器:如实告诉面板,让它把授权链接交给用户手动打开。
263
+ return { opened: false, reason: opened.reason || "system-opener-unavailable" };
239
264
  } catch (error) {
240
265
  return { opened: false, reason: String(error?.message || error) };
241
266
  }
@@ -600,6 +625,11 @@ async function checkForUpdate(now = Date.now(), force = false) {
600
625
  return result;
601
626
  }
602
627
 
628
+ // 凭据类失败(401/403 / 令牌解析失败 / OAuth 报错)与「进程起不来、网络不通」要分开说:
629
+ // 前者重连多少次都不会恢复,面板应显示「需授权」并把用户送去授权的入口。这条正则只用于
630
+ // 展示态的细分,不参与任何控制流;匹配不到就保持原本的「连接失败」。
631
+ const AUTH_FAILURE_RE = /(401|403|unauthori[sz]ed|forbidden|invalid[ _-]?token|access token|token parse|oauth|bearer|鉴权|认证|授权)/i;
632
+
603
633
  function summarize(entry, inventory, managed, conflict = false, lastLog = null) {
604
634
  const live = entry.options.config;
605
635
  const raw = managed || live;
@@ -617,6 +647,8 @@ function summarize(entry, inventory, managed, conflict = false, lastLog = null)
617
647
  row.lastError = row.status !== "failed" ? null : lastLog && (lastLog.type === "error" || lastLog.type === "warn")
618
648
  ? sanitizeMcpLog(lastLog.text)
619
649
  : "MCP 未注册任何工具:连接失败或 tools/list 同步失败";
650
+ // 401/403 这类失败重连不会恢复:面板据此把「连接失败」细分成「需授权」。
651
+ row.authRequired = row.status === "failed" && AUTH_FAILURE_RE.test(`${row.lastError || ""} ${lastLog?.text || ""}`);
620
652
  return row;
621
653
  }
622
654
  // 项目 MCP 的行状态:与全局 summarize() 走**同一套规则、同一个判定函数**(deriveMcpPhase),
@@ -668,6 +700,8 @@ function summarizeWorkspaceRow(ctx, wsPath, server, mountError) {
668
700
  row.lastError = row.lastError ? `${notice};${row.lastError}` : notice;
669
701
  }
670
702
  }
703
+ row.authRequired = row.status === "failed" && !row.mountFailed && !row.scopeFailed
704
+ && AUTH_FAILURE_RE.test(`${row.lastError || ""} ${lastLog?.text || ""}`);
671
705
  return row;
672
706
  }
673
707
  // Fingerprint the exact redacted wire projection. Keeping a manual field list here
@@ -973,8 +1007,36 @@ function workspaceServerView(server) {
973
1007
  // 2) 项目 MCP 服务器名与禁用状态(各工作区 .dsh/mcp.json,只读);
974
1008
  // 3) 当前注册表里的全部工具 Schema(ctx.tools.schemas(),全局 + 项目 + 内置)。
975
1009
  // 只读操作:不写任何配置、不碰会话目录。禁用/启用动作复用下方既有的 disable/enable。
1010
+ // P2-1:账本缓存。30 天口径在本机实测 1.7s(6542 条事件),而看板每次挂载、每次切
1011
+ // 天数都会重扫一遍;5 分钟内同 days 直接回上次结果。并发首次调用共享同一条扫描
1012
+ // (看板与 toolTailor 页可能同时挂载),避免同一份日志被解两遍。
1013
+ // days 在这里收敛到 [1,365]:缓存键就是 days,不能让 0 / 负数 / 超大值各自成键。
1014
+ const TAILOR_CACHE_TTL = 5 * 60 * 1000;
1015
+ const tailorCache = new Map(); // days -> { at, report }
1016
+ const tailorInFlight = new Map(); // days -> Promise<report>
976
1017
  async function buildToolTailorReport(ctx, payload) {
977
- const days = Math.trunc(Number(payload?.days) || 30);
1018
+ const days = Math.min(365, Math.max(1, Math.trunc(Number(payload?.days) || 30)));
1019
+ const refresh = payload?.refresh === true;
1020
+ if (!refresh) {
1021
+ const hit = tailorCache.get(days);
1022
+ if (hit && Date.now() - hit.at < TAILOR_CACHE_TTL) return hit.report;
1023
+ const pending = tailorInFlight.get(days);
1024
+ if (pending) return pending;
1025
+ }
1026
+ const task = scanToolTailor(ctx, days)
1027
+ .then((report) => {
1028
+ tailorCache.set(days, { at: Date.now(), report });
1029
+ return report;
1030
+ })
1031
+ .finally(() => {
1032
+ if (tailorInFlight.get(days) === task) tailorInFlight.delete(days);
1033
+ });
1034
+ tailorInFlight.set(days, task);
1035
+ return task;
1036
+ }
1037
+
1038
+ /** 真正干活的那次扫描(缓存旁路 / 未命中时调用)。 */
1039
+ async function scanToolTailor(ctx, days) {
978
1040
  const mcpServerNames = [];
979
1041
  const disabledServers = new Set();
980
1042
  for (const entry of allMcpEntries(ctx)) {
@@ -1030,9 +1092,21 @@ async function attachRegisteredSkills(ctx, report) {
1030
1092
  const available = registered.length > 0;
1031
1093
  const registeredNames = new Set(registered.map((skill) => skill.name));
1032
1094
  const observedNames = new Set(observed.map((skill) => skill.name));
1095
+ // 禁用/释放按钮需要的三个判定字段跟着注册表行走:path/source 决定「能不能写」
1096
+ // (白名单口径见 lib/skill-disable.js),disabled 直接读宿主解析出的 invocation 位。
1097
+ // 未注册的行(注册表缺席 / 已卸载技能的历史调用)不带这些字段——客户端不给它出按钮。
1098
+ const control = (skill) => {
1099
+ if (!skill) return {};
1100
+ return {
1101
+ disabled: skill.invocation?.modelInvocable === false,
1102
+ ...(typeof skill.path === "string" && skill.path ? { path: skill.path } : {}),
1103
+ ...(typeof skill.source === "string" && skill.source ? { source: skill.source } : {}),
1104
+ };
1105
+ };
1106
+ const registeredByName = new Map(registered.map((skill) => [skill.name, skill]));
1033
1107
  const merged = available
1034
1108
  ? [
1035
- ...observed.map((skill) => ({ ...skill, registered: registeredNames.has(skill.name) })),
1109
+ ...observed.map((skill) => ({ ...skill, registered: registeredNames.has(skill.name), ...control(registeredByName.get(skill.name)) })),
1036
1110
  ...registered
1037
1111
  .filter((skill) => !observedNames.has(skill.name))
1038
1112
  .map((skill) => ({
@@ -1043,6 +1117,7 @@ async function attachRegisteredSkills(ctx, report) {
1043
1117
  firstUsedMs: null,
1044
1118
  lastUsedMs: null,
1045
1119
  registered: true,
1120
+ ...control(skill),
1046
1121
  })),
1047
1122
  ].sort((a, b) => (b.calls - a.calls) || String(a.name).localeCompare(String(b.name)))
1048
1123
  : observed.map((skill) => ({ ...skill, registered: null }));
@@ -1178,10 +1253,11 @@ async function collectLoadoutItems(ctx) {
1178
1253
  // 3) 专家:profile 补丁里的召唤预设 + 目录缓存补展示名 / 更新时间。
1179
1254
  try {
1180
1255
  const patch = await readPatch(ctx);
1181
- const presets = readExpertPresets(patch.content);
1256
+ const expertApi = await loadExpert();
1257
+ const presets = expertApi.readExpertPresets(patch.content);
1182
1258
  let experts = [];
1183
1259
  try {
1184
- const catalog = await wbExpertCatalog();
1260
+ const catalog = await expertApi.wbExpertCatalog();
1185
1261
  experts = Array.isArray(catalog?.experts) ? catalog.experts : [];
1186
1262
  } catch {
1187
1263
  // 目录拉不到就退回 plugin id 作名称。
@@ -1224,7 +1300,7 @@ async function collectLoadoutItems(ctx) {
1224
1300
  let McpManagerGateway = (() => {
1225
1301
  let _classSuper = TypertRemoteService;
1226
1302
  let _instanceExtraInitializers = [];
1227
- let _list_decorators, _status_decorators, _enable_decorators, _disable_decorators, _reconnect_decorators, _tools_decorators, _add_decorators, _update_decorators, _remove_decorators, _reveal_decorators, _previewImport_decorators, _importJson_decorators, _listWorkspaces_decorators, _getWorkspaceView_decorators, _addWorkspaceServer_decorators, _updateWorkspaceServer_decorators, _removeWorkspaceServer_decorators, _setWorkspaceExclude_decorators, _previewWorkspaceImport_decorators, _importWorkspaceJson_decorators, _revealWorkspaceServer_decorators, _projectConnections_decorators, _reconnectWorkspaceServer_decorators, _updateHint_decorators, _whatsNew_decorators, _scanImportSources_decorators, _previewImportSource_decorators, _importSource_decorators, _toolTailor_decorators, _wbConnectorCatalog_decorators, _wbConnectorCachedCount_decorators, _wbConnectorIcon_decorators, _wbConnectorPreview_decorators, _wbConnectorInstall_decorators, _wbOAuthBegin_decorators, _wbOAuthPoll_decorators, _wbOAuthCancel_decorators, _wbOAuthToken_decorators, _wbOAuthOverview_decorators, _wbOAuthOpen_decorators, _wbSkillCatalog_decorators, _wbSkillDetail_decorators, _wbSkillInstall_decorators, _wbSkillUninstall_decorators, _wbSkillInstalled_decorators, _wbMarketRegistry_decorators, _wbMarketRegistryUpsert_decorators, _wbInstallState_decorators, _wbInstallStateRecord_decorators, _wbInstallStateForget_decorators, _wbAwesomeCatalog_decorators, _wbAwesomeDetail_decorators, _wbAwesomeReadme_decorators, _wbAwesomeUpdateCheck_decorators, _wbRepoMeta_decorators, _wbNpmDownloads_decorators, _wbLoadoutExport_decorators, _wbLoadoutDiff_decorators;
1303
+ let _list_decorators, _status_decorators, _enable_decorators, _disable_decorators, _reconnect_decorators, _tools_decorators, _add_decorators, _update_decorators, _remove_decorators, _reveal_decorators, _previewImport_decorators, _importJson_decorators, _listWorkspaces_decorators, _getWorkspaceView_decorators, _addWorkspaceServer_decorators, _updateWorkspaceServer_decorators, _removeWorkspaceServer_decorators, _setWorkspaceExclude_decorators, _previewWorkspaceImport_decorators, _importWorkspaceJson_decorators, _revealWorkspaceServer_decorators, _projectConnections_decorators, _reconnectWorkspaceServer_decorators, _updateHint_decorators, _whatsNew_decorators, _scanImportSources_decorators, _previewImportSource_decorators, _importSource_decorators, _toolTailor_decorators, _wbConnectorCatalog_decorators, _wbConnectorCachedCount_decorators, _wbConnectorIcon_decorators, _wbConnectorPreview_decorators, _wbConnectorInstall_decorators, _wbOAuthBegin_decorators, _wbOAuthPoll_decorators, _wbOAuthCancel_decorators, _wbOAuthToken_decorators, _wbOAuthOverview_decorators, _wbOAuthRefresh_decorators, _wbOAuthOpen_decorators, _wbSkillCatalog_decorators, _wbSkillDetail_decorators, _wbSkillInstall_decorators, _wbSkillUninstall_decorators, _wbSkillInstalled_decorators, _wbMarketRegistry_decorators, _wbMarketRegistryUpsert_decorators, _wbInstallState_decorators, _wbInstallStateRecord_decorators, _wbInstallStateForget_decorators, _wbAwesomeCatalog_decorators, _wbAwesomeDetail_decorators, _wbAwesomeReadme_decorators, _wbAwesomeUpdateCheck_decorators, _wbRepoMeta_decorators, _wbNpmDownloads_decorators, _wbLoadoutExport_decorators, _wbLoadoutDiff_decorators;
1228
1304
  return class McpManagerGateway extends _classSuper {
1229
1305
  static {
1230
1306
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
@@ -1306,6 +1382,8 @@ let McpManagerGateway = (() => {
1306
1382
  __esDecorate(this, null, _wbOAuthToken_decorators, { kind: "method", name: "wbOAuthToken", static: false, private: false, access: { has: (obj) => "wbOAuthToken" in obj, get: (obj) => obj.wbOAuthToken }, metadata: _metadata }, null, _instanceExtraInitializers);
1307
1383
  _wbOAuthOverview_decorators = [Remote("wbOAuthOverview")];
1308
1384
  __esDecorate(this, null, _wbOAuthOverview_decorators, { kind: "method", name: "wbOAuthOverview", static: false, private: false, access: { has: (obj) => "wbOAuthOverview" in obj, get: (obj) => obj.wbOAuthOverview }, metadata: _metadata }, null, _instanceExtraInitializers);
1385
+ _wbOAuthRefresh_decorators = [Remote("wbOAuthRefresh")];
1386
+ __esDecorate(this, null, _wbOAuthRefresh_decorators, { kind: "method", name: "wbOAuthRefresh", static: false, private: false, access: { has: (obj) => "wbOAuthRefresh" in obj, get: (obj) => obj.wbOAuthRefresh }, metadata: _metadata }, null, _instanceExtraInitializers);
1309
1387
  _wbOAuthOpen_decorators = [Remote("wbOAuthOpen")];
1310
1388
  __esDecorate(this, null, _wbOAuthOpen_decorators, { kind: "method", name: "wbOAuthOpen", static: false, private: false, access: { has: (obj) => "wbOAuthOpen" in obj, get: (obj) => obj.wbOAuthOpen }, metadata: _metadata }, null, _instanceExtraInitializers);
1311
1389
  _wbSkillCatalog_decorators = [Remote("wbSkillCatalog")];
@@ -1354,6 +1432,13 @@ let McpManagerGateway = (() => {
1354
1432
  super(ctx, "mcpManager");
1355
1433
  ensureLogCapture(ctx);
1356
1434
  installAgentRuntime(ctx);
1435
+ // 会话级专家「记住选择」:agent 重建(切会话/重启/压缩)后自动补回人设。
1436
+ // 与项目 MCP 的安装完全独立;失败只能降级(不会自动恢复),不能拖垮插件装配。
1437
+ try {
1438
+ installPersonaRuntime(ctx);
1439
+ } catch (error) {
1440
+ ctx.logger?.warn?.(`armory: 会话专家运行时安装失败:${error?.message || error}`);
1441
+ }
1357
1442
  __runInitializers(this, _instanceExtraInitializers);
1358
1443
  }
1359
1444
  async list() {
@@ -1521,6 +1606,7 @@ let McpManagerGateway = (() => {
1521
1606
  async toolTailor(payload) {
1522
1607
  // 只读账本:扫描本机会话日志 + 当前注册表 Schema,见 tool-ledger.js。
1523
1608
  // days 由调用方传(7/30/90),缺省 30,服务端钳到 [1,365]。
1609
+ // 5 分钟缓存命中直接回上次报告;用户点「重扫」时传 refresh:true 强制重算。
1524
1610
  // 「清空解压文本缓存」这个可选动作已删(2026-10-23):它曾声称「先清后扫,面板立即
1525
1611
  // 看到占用归零」,实测恰好相反 —— 快照取在扫描之后,同一发调用里日志被重新解压回填,
1526
1612
  // 前后都是 32 条 / 65.8 MB;而 clearTextCache() 的返回值(clearedBytes)压根没进报告
@@ -1791,15 +1877,40 @@ let McpManagerGateway = (() => {
1791
1877
  return { skill: result };
1792
1878
  }
1793
1879
  async revealSkill(payload) {
1794
- const path = String(payload?.path || "");
1795
- if (!path) throw new Error("该 skill 没有可打开的文件路径(运行时注册的 skill 不在磁盘上)");
1880
+ // 路径只信宿主 skill 注册表的解析结果,不信客户端传来的 payload.path:
1881
+ // 否则面板能借「打开目录」把任意路径交给 explorer / open 定位。
1882
+ const name = String(payload?.name || "").trim();
1883
+ if (!name) throw new Error("缺少 skill 名称");
1884
+ const resolved = await withSkillScope(this.ctx, payload, async (registry, options) => {
1885
+ if (typeof registry.get !== "function") throw new Error("宿主没有挂载 skill 注册表(@deepseek-ai/dsh-skill),无法定位 skill");
1886
+ const skill = await registry.get(name, options);
1887
+ if (!skill) throw new Error("没有找到这个 skill(可能刚被移动或删除):" + name);
1888
+ return String(skill.path || "");
1889
+ });
1890
+ if (resolved === undefined) throw new Error("宿主没有挂载 skill 注册表(@deepseek-ai/dsh-skill),无法定位 skill");
1891
+ const path = String(resolved);
1892
+ if (!path || path.includes("\0") || !isAbsolute(path)) throw new Error("该 skill 没有可打开的文件路径(运行时注册的 skill 不在磁盘上)");
1796
1893
  const shell = typeof this.ctx.get === "function" ? this.ctx.get("shell") : undefined;
1797
- if (shell && typeof shell.showItemInFolder === "function") { shell.showItemInFolder(path); return { note: "已在资源管理器中定位" }; }
1798
- if (shell && typeof shell.openExternal === "function") { shell.openExternal(path); return { note: "已交给宿主打开" }; }
1799
- const { spawn } = await import("node:child_process");
1800
- if (process.platform === "win32") spawn("explorer", ["/select,", path], { detached: true, stdio: "ignore" }).unref();
1801
- else spawn(process.platform === "darwin" ? "open" : "xdg-open", [path], { detached: true, stdio: "ignore" }).unref();
1802
- return { note: "已交给系统打开" };
1894
+ if (shell && typeof shell.showItemInFolder === "function") {
1895
+ try {
1896
+ await shell.showItemInFolder(path);
1897
+ return { note: "已在资源管理器中定位" };
1898
+ } catch {
1899
+ /* 宿主定位失败时落到系统兜底 */
1900
+ }
1901
+ }
1902
+ if (shell && typeof shell.openExternal === "function") {
1903
+ try {
1904
+ await shell.openExternal(path);
1905
+ return { note: "已交给宿主打开" };
1906
+ } catch {
1907
+ /* 宿主打开失败时落到系统兜底 */
1908
+ }
1909
+ }
1910
+ const opened = await revealSystemPath(path, { logger: this.ctx?.logger });
1911
+ if (opened.ok) return { note: "已交给系统打开" };
1912
+ // headless 主机没有桌面文件管理器:不能谎报「已打开」,把真实路径交回去。
1913
+ return { note: `没有可用的系统打开器(${opened.reason}),路径:${path}`, path, opened: false };
1803
1914
  }
1804
1915
  // 卸载一条已安装 skill:永久删除用户级/项目级 skills 根目录内的条目。
1805
1916
  // 只信注册表给的 source/path(客户端传的路径不参与判断);bundled / runtime / custom
@@ -1817,6 +1928,36 @@ let McpManagerGateway = (() => {
1817
1928
  if (result === undefined) throw new Error("宿主没有挂载 skill 注册表(@deepseek-ai/dsh-skill),无法卸载");
1818
1929
  return { note: `已卸载 ${name}(${result.kind === "symlink" ? "符号链接" : "目录"}:${result.path})` };
1819
1930
  }
1931
+ // 禁用 / 释放一条 skill(工具裁缝技能账本的行内按钮):写 SKILL.md frontmatter 的
1932
+ // disable-model-invocation 标志——宿主 skill-filesystem 官方支持的「模型侧禁用」键,
1933
+ // 根目录监听器会自动刷新模型目录(无需重启)。禁用后技能不再进模型目录、不再缴
1934
+ // 上下文税;输入框手动 /引用 不受影响。写盘规则与安全闸门全在 lib/skill-disable.js
1935
+ // (来源白名单 / 根目录包含性 / realpath 防链接出根),与 removeSkill 同一套纪律。
1936
+ async disableSkill(payload) {
1937
+ return this.setSkillDisabled(payload, true);
1938
+ }
1939
+ async enableSkill(payload) {
1940
+ return this.setSkillDisabled(payload, false);
1941
+ }
1942
+ async setSkillDisabled(payload, disabled) {
1943
+ const name = String(payload?.name || "").trim();
1944
+ if (!name) throw new Error("缺少 skill 名称");
1945
+ const result = await withSkillScope(this.ctx, payload, async (registry, options) => {
1946
+ if (typeof registry.get !== "function") throw new Error("宿主没有挂载 skill 注册表(@deepseek-ai/dsh-skill),无法修改");
1947
+ const skill = await registry.get(name, options);
1948
+ if (!skill) throw new Error("没有找到这个 skill(可能刚被移动或删除):" + name);
1949
+ return setSkillModelInvocationOnDisk(skill, { disabled, cwd: options.cwd, home: homedir() });
1950
+ });
1951
+ if (result === undefined) throw new Error("宿主没有挂载 skill 注册表(@deepseek-ai/dsh-skill),无法修改");
1952
+ // 幂等重放(changed=false)也照常报成功:按钮态以注册表实况为准,重放只是补一次确认。
1953
+ return {
1954
+ note: disabled
1955
+ ? `已禁用 ${name}(模型不再加载,手动引用不受影响)`
1956
+ : `已释放 ${name}(模型可重新调用)`,
1957
+ path: result.path,
1958
+ changed: result.changed,
1959
+ };
1960
+ }
1820
1961
  // 安装时间 + 市场原名(按 skill 名字索引):宿主自扫标准技能根目录、读 SKILL.md
1821
1962
  // 头部 name 建立映射——不依赖 listSkills 行里的 path(注册表可能整列不给)。
1822
1963
  // 永不抛错——时间与原名是辅助信息,单个目录读失败只是没有该条目,不该拖垮看板。
@@ -1847,18 +1988,22 @@ let McpManagerGateway = (() => {
1847
1988
  // 连接器安装复用 applyManagedImport / applyWorkspaceImport:准入、同名跳过、entry id
1848
1989
  // 预留、项目对齐全部与粘贴导入同一条链路,市场只是多了一个「JSON 从哪来」的来源。
1849
1990
  async wbConnectorCatalog() {
1850
- return wbConnectorCatalog();
1991
+ const marketApi = await loadMarket();
1992
+ return marketApi.wbConnectorCatalog();
1851
1993
  }
1852
1994
  // 顶栏「市场」chip 的常驻计数:只读磁盘缓存,绝不联网(无缓存返回 null)。
1853
1995
  async wbConnectorCachedCount() {
1854
- return wbConnectorCachedCount();
1996
+ const marketApi = await loadMarket();
1997
+ return marketApi.wbConnectorCachedCount();
1855
1998
  }
1856
1999
  async wbConnectorPreview(payload) {
1857
- return wbConnectorPreview(payload?.connectorId);
2000
+ const marketApi = await loadMarket();
2001
+ return marketApi.wbConnectorPreview(payload?.connectorId);
1858
2002
  }
1859
2003
  // 某条连接器的图标(data URL)。市场卡片进视口才调,payload 形状同 wbConnectorPreview。
1860
2004
  async wbConnectorIcon(payload) {
1861
- return wbConnectorIcon(payload?.connectorId);
2005
+ const marketApi = await loadMarket();
2006
+ return marketApi.wbConnectorIcon(payload?.connectorId);
1862
2007
  }
1863
2008
  async wbConnectorInstall(payload) {
1864
2009
  const connectorId = String(payload?.connectorId || "").trim();
@@ -1866,17 +2011,35 @@ let McpManagerGateway = (() => {
1866
2011
  const mode = payload?.mode === "replace" ? "replace" : "merge";
1867
2012
  const wsPath = payload?.wsPath ? canonicalWorkspacePath(String(payload.wsPath)) : "";
1868
2013
  const tokens = payload?.tokens && typeof payload.tokens === "object" ? payload.tokens : {};
1869
- const preview = await wbConnectorPreview(connectorId);
2014
+ const marketApi = await loadMarket();
2015
+ const preview = await marketApi.wbConnectorPreview(connectorId);
1870
2016
  // T04/R7:把面板收集到的 token 填进 ${...} 占位符。只填用户明确提供的槽位,
1871
2017
  // 未填的保持原样并把槽位名回传面板(missingSlots)——绝不代填、绝不猜值。
1872
2018
  const { mcpServers, missingSlots } = applyTokens(preview.mcpServers, tokens);
1873
2019
  // R8:先把已授权的 OAuth token 注进 headers 再探测——否则授权过的连接器每次重装
1874
2020
  // 都会被判成「需要授权」,用户得反复点授权。
1875
2021
  const authorized = await injectStoredOAuthTokens(mcpServers);
2022
+ // 本机固定依赖预检:本地客户端型连接器(如 nvapp-windows-local)写死一个本机可执行文件。
2023
+ // 缺这个文件时写进配置只会让宿主重连 10 次后注销工具(issue #3),所以安装前直接拦住,
2024
+ // 并把「缺哪个路径 / 平台是否支持」作为结构化字段回传面板做本地化提示。
2025
+ const localDeps = await marketApi.connectorLocalDeps(authorized);
2026
+ const localMissing = localDeps.filter((dep) => dep.status === "missing").map((dep) => dep.path);
2027
+ const localUnsupported = localDeps.filter((dep) => dep.status === "platform").map((dep) => dep.path);
2028
+ if (localMissing.length || localUnsupported.length) {
2029
+ return {
2030
+ ok: false,
2031
+ connectorId,
2032
+ missingSlots,
2033
+ localDeps,
2034
+ localMissing,
2035
+ localUnsupported,
2036
+ error: `本机缺少连接器依赖:${[...localMissing, ...localUnsupported].join("、")}`,
2037
+ };
2038
+ }
1876
2039
  // 装前 401 探测:索引 auth_mode 会漏标鉴权(tec-do 裸 URL 装完必 401)。断言从
1877
2040
  // 「任一不就整体中止」改为「回传需授权清单」:面板据此弹浏览器授权页,授权完成
1878
2041
  // 后重装即可,绝不再抛「DSH 无法自动注入」把用户挡在门外。
1879
- const probe = await probeMcpServersAuthorization(authorized);
2042
+ const probe = await marketApi.probeMcpServersAuthorization(authorized);
1880
2043
  if (probe?.needsAuth?.length) {
1881
2044
  return {
1882
2045
  ok: false,
@@ -1890,8 +2053,27 @@ let McpManagerGateway = (() => {
1890
2053
  const result = wsPath
1891
2054
  ? await applyWorkspaceImport(this.ctx, wsPath, admittedImport(json), mode)
1892
2055
  : await applyManagedImport(this.ctx, admittedImport(json), mode);
1893
- // T04/R8:装成功后登记本地状态(供「已安装」全量筛选与来源锁)。登记失败只记
1894
- // 日志、绝不影响安装结果——状态文件坏了不该让用户白装一次。
2056
+ // T04/R8:只有**真的写进了 server** 才登记「已安装」。同名条目被其他配置层占用时,
2057
+ // applyManagedImport 会跳过它并返回空 added/updated——早前这里无条件登记,于是一条
2058
+ // 根本没落盘的连接器也带着「已安装」徽标(用户报的「显示装好了却用不了」就是这个
2059
+ // 徽标在说谎)。跳过 ≠ 装好:这里如实回失败,并把跳过原因带回面板。
2060
+ const wrote = (Array.isArray(result?.added) ? result.added.length : 0)
2061
+ + (Array.isArray(result?.updated) ? result.updated.length : 0);
2062
+ if (!wrote) {
2063
+ const skipped = Array.isArray(result?.warnings) ? result.warnings.filter(Boolean) : [];
2064
+ return {
2065
+ ok: false,
2066
+ connectorId,
2067
+ missingSlots,
2068
+ localDeps,
2069
+ wrote: 0,
2070
+ skipped,
2071
+ error: skipped.length
2072
+ ? `没有写入任何 MCP 配置:${skipped.join(";")}`
2073
+ : "没有写入任何 MCP 配置:目标作用域里没有可写入的 server",
2074
+ };
2075
+ }
2076
+ // 登记失败只记日志、绝不影响安装结果——状态文件坏了不该让用户白装一次。
1895
2077
  try {
1896
2078
  await recordInstallState({
1897
2079
  kind: "connector",
@@ -1904,7 +2086,7 @@ let McpManagerGateway = (() => {
1904
2086
  } catch (error) {
1905
2087
  this.ctx.logger?.warn?.(`wb-market: 连接器 ${connectorId} 安装状态登记失败(不影响安装结果):${error?.message || error}`);
1906
2088
  }
1907
- return { ...result, missingSlots };
2089
+ return { ...result, missingSlots, localDeps, wrote };
1908
2090
  }
1909
2091
  // ── R8:连接器 MCP 的浏览器授权(RFC 9728 / 8414 / 7591 / 7636)──────────
1910
2092
  // 面板只负责展示与点击:安装前探测到 401 → 这里拿到 authorizeUrl 交给系统浏览器,
@@ -1986,20 +2168,68 @@ let McpManagerGateway = (() => {
1986
2168
  const overview = await oauthOverview(target.url).catch(() => null);
1987
2169
  return { ok: true, connectorId, serverName: target.serverName, ...(overview || {}) };
1988
2170
  }
2171
+ // R8/P0:强制刷新已存的 OAuth token(有 refresh_token 才刷得动)。面板的「刷新授权」
2172
+ // 走这里,再调用一次 wbConnectorInstall 把新凭据写回 headers——mcp-client 只在建立连接时
2173
+ // 快照 headers,光刷令牌库不改配置等于没刷。
2174
+ // 刷不动(没有 refresh_token / 已被服务端撤销)时回 needsAuth:true,让面板回到浏览器授权,
2175
+ // 而不是给一条「刷新失败」的死胡同。
2176
+ async wbOAuthRefresh(payload) {
2177
+ const connectorId = String(payload?.connectorId || "").trim();
2178
+ if (!connectorId) throw new Error("缺少 connectorId");
2179
+ const target = await resolveOAuthTarget(connectorId, payload?.serverName);
2180
+ const before = await oauthOverview(target.url).catch(() => null);
2181
+ if (!before?.authorized) {
2182
+ return {
2183
+ ok: false,
2184
+ connectorId,
2185
+ serverName: target.serverName,
2186
+ authorized: false,
2187
+ needsAuth: true,
2188
+ error: "该连接器还没有授权记录,请先完成授权",
2189
+ };
2190
+ }
2191
+ const ensured = await oauthEnsureToken(target.url, { force: true })
2192
+ .catch((error) => ({ ok: false, error: error?.message || String(error) }));
2193
+ const after = await oauthOverview(target.url).catch(() => null);
2194
+ if (!ensured?.ok) {
2195
+ return {
2196
+ ok: false,
2197
+ connectorId,
2198
+ serverName: target.serverName,
2199
+ authorized: Boolean(after?.authorized),
2200
+ needsAuth: true,
2201
+ refreshable: Boolean(after?.refreshable),
2202
+ expiresAt: after?.expiresAt || "",
2203
+ error: ensured?.error || "刷新授权失败",
2204
+ };
2205
+ }
2206
+ return {
2207
+ ok: true,
2208
+ connectorId,
2209
+ serverName: target.serverName,
2210
+ authorized: true,
2211
+ refreshed: ensured.refreshed === true,
2212
+ expiresAt: after?.expiresAt || "",
2213
+ refreshable: Boolean(after?.refreshable),
2214
+ };
2215
+ }
1989
2216
  async wbOAuthOpen(payload) {
1990
2217
  const opened = await openExternalUrl(this.ctx, payload?.url);
1991
2218
  return { ok: opened.opened === true, ...opened };
1992
2219
  }
1993
2220
  async wbSkillCatalog(payload) {
1994
- return wbSkillCatalog(payload || {});
2221
+ const marketApi = await loadMarket();
2222
+ return marketApi.wbSkillCatalog(payload || {});
1995
2223
  }
1996
2224
  async wbSkillDetail(payload) {
1997
- return wbSkillDetail(payload || {});
2225
+ const marketApi = await loadMarket();
2226
+ return marketApi.wbSkillDetail(payload || {});
1998
2227
  }
1999
2228
  async wbSkillInstall(payload) {
2229
+ const marketApi = await loadMarket();
2000
2230
  // 安装进度经 ctx.logger 落到宿主日志(Remote 单次调用没有流式回传通道,面板按
2001
2231
  // 阶段提示「正在下载与校验」,完成后一次拿到结果与 note)。
2002
- const result = await wbSkillInstall(payload || {}, {
2232
+ const result = await marketApi.wbSkillInstall(payload || {}, {
2003
2233
  onProgress: (progress) => this.ctx.logger?.debug?.(`wb-market: ${progress.phase} ${progress.index}/${progress.total}`),
2004
2234
  });
2005
2235
  // T04/R8:装成功后登记本地状态。key 取不到就跳过(record 对空 key 会抛错)。
@@ -2021,10 +2251,12 @@ let McpManagerGateway = (() => {
2021
2251
  return result;
2022
2252
  }
2023
2253
  async wbSkillUninstall(payload) {
2024
- return wbSkillUninstall(payload || {});
2254
+ const marketApi = await loadMarket();
2255
+ return marketApi.wbSkillUninstall(payload || {});
2025
2256
  }
2026
2257
  async wbSkillInstalled() {
2027
- return wbSkillInstalled();
2258
+ const marketApi = await loadMarket();
2259
+ return marketApi.wbSkillInstalled();
2028
2260
  }
2029
2261
  // ── T03/R4:多市场注册表 ────────────────────────────────────────────
2030
2262
  // 数据层在 lib/wb-registry.js(T01);这里只做 RPC 转发。读 = 合并投影(内置默认
@@ -2068,6 +2300,12 @@ let McpManagerGateway = (() => {
2068
2300
  async wbAwesomeUpdateCheck() {
2069
2301
  return wbAwesomeUpdateCheck();
2070
2302
  }
2303
+ // ── 社区 MCP(官方 MCP Registry v0,只读)──────────────────────────
2304
+ // 数据层在 lib/mcp-community.js;这里只做 RPC 转发。只 GET:不写盘、不执行命令、
2305
+ // 不落任何凭据。payload {search, cursor, limit, takenNames},形状由数据层校验。
2306
+ async wbMcpCommunityList(payload) {
2307
+ return wbMcpCommunityList(payload || {});
2308
+ }
2071
2309
  // ── 社区元信息:仓库「人味」信号(维护者 / 最近提交 / issue / star)────────
2072
2310
  // 详情弹层按需调用,只读公开接口,6 小时缓存;不接收也不保存任何凭据。
2073
2311
  async wbRepoMeta(payload) {
@@ -2109,19 +2347,26 @@ let McpManagerGateway = (() => {
2109
2347
  const plugin = String(payload?.plugin || "").trim();
2110
2348
  if (!sessionId) throw new Error("缺少 sessionId");
2111
2349
  if (!plugin) throw new Error("缺少 plugin");
2112
- const detail = await wbExpertDetail({ plugin });
2350
+ const expertApi = await loadExpert();
2351
+ const detail = await expertApi.wbExpertDetail({ plugin });
2113
2352
  // 团队型:成员人设先落盘,主理人人设末尾追加 DSH 桥接段(与召唤预设同一套)。
2114
- const prepared = await prepareExpertPersona(detail, {
2353
+ const prepared = await expertApi.prepareExpertPersona(detail, {
2115
2354
  onLog: (message) => this.ctx.logger?.debug?.(`wb-expert: ${message}`),
2116
2355
  });
2117
2356
  const result = attachPersona(this.ctx, sessionId, {
2118
2357
  plugin,
2119
2358
  name: detail.name,
2120
2359
  personaText: prepared.personaText,
2360
+ }, {
2361
+ // 替身人设不写进 session-persona 的粘性选择:它只是原文缺失时的占位,
2362
+ // 落盘后即使上游补回原文也会一直沿用。重启后重新召唤即可拿到最新正文。
2363
+ persist: detail.personaSource !== "generated",
2121
2364
  });
2122
- await recordExpertRecent("expert", { plugin, name: detail.name, profession: detail.profession, presetId: `session-${plugin}` }).catch(() => {});
2365
+ await expertApi.recordExpertRecent("expert", { plugin, name: detail.name, profession: detail.profession, presetId: `session-${plugin}` }).catch(() => {});
2123
2366
  return {
2124
2367
  ...result,
2368
+ personaSource: detail.personaSource,
2369
+ personaNotice: detail.personaNotice,
2125
2370
  profession: detail.profession,
2126
2371
  expertType: detail.expertType,
2127
2372
  members: detail.members || [],
@@ -2138,31 +2383,34 @@ let McpManagerGateway = (() => {
2138
2383
  return { sessions: listPersonas() };
2139
2384
  }
2140
2385
  async wbExpertCatalog() {
2141
- return wbExpertCatalog();
2386
+ const expertApi = await loadExpert();
2387
+ return expertApi.wbExpertCatalog();
2142
2388
  }
2143
2389
  async wbExpertDetail(payload) {
2144
- return wbExpertDetail(payload || {});
2390
+ const expertApi = await loadExpert();
2391
+ return expertApi.wbExpertDetail(payload || {});
2145
2392
  }
2146
2393
  async wbExpertSummon(payload) {
2147
2394
  const plugin = String(payload?.plugin || "").trim();
2148
2395
  if (!plugin) throw new Error("缺少 plugin");
2396
+ const expertApi = await loadExpert();
2149
2397
  // 详情即预览:召唤写盘前拉一次真实人设与技能清单,失败则整体不动。
2150
- const detail = await wbExpertDetail({ plugin });
2398
+ const detail = await expertApi.wbExpertDetail({ plugin });
2151
2399
  const withSkills = payload?.withSkills !== false && detail.skills.length > 0;
2152
2400
  // 团队型:成员人设先落盘(人设不进 preset,避免每轮白付),主理人人设追加桥接段。
2153
- const prepared = await prepareExpertPersona(detail, {
2401
+ const prepared = await expertApi.prepareExpertPersona(detail, {
2154
2402
  onLog: (message) => this.ctx.logger?.debug?.(`wb-expert: ${message}`),
2155
2403
  });
2156
2404
  // 技能先落盘(失败只影响技能,不阻断召唤),预设行再走唯一写链(文件锁 + 版本校验 + 原子写)。
2157
2405
  let skillResult = { installed: [], notes: [] };
2158
2406
  if (withSkills) {
2159
- skillResult = await installExpertSkills(plugin, detail.skills.map((skill) => skill.name), {
2407
+ skillResult = await expertApi.installExpertSkills(plugin, detail.skills.map((skill) => skill.name), {
2160
2408
  onLog: (message) => this.ctx.logger?.debug?.(`wb-expert: ${message}`),
2161
2409
  });
2162
2410
  }
2163
2411
  const result = await withPatchWrite(this.ctx, async () => {
2164
2412
  const state = await managedState(this.ctx);
2165
- const written = writeExpertPreset(state.content, {
2413
+ const written = expertApi.writeExpertPreset(state.content, {
2166
2414
  plugin,
2167
2415
  displayName: detail.name,
2168
2416
  profession: detail.profession,
@@ -2174,11 +2422,11 @@ let McpManagerGateway = (() => {
2174
2422
  return { mode: written.mode, presetId: written.presetId };
2175
2423
  });
2176
2424
  // 召唤成功即记入最近使用(送还由 listExpertRecent 对账剔除)。记录失败不阻断召唤。
2177
- await recordExpertRecent("expert", { plugin, name: detail.name, profession: detail.profession, presetId: result.presetId }).catch(() => {});
2425
+ await expertApi.recordExpertRecent("expert", { plugin, name: detail.name, profession: detail.profession, presetId: result.presetId }).catch(() => {});
2178
2426
  const teamMembers = prepared.memberResult?.installed || [];
2179
2427
  // upsert 后核对人设正文是否真带桥接段(团队型);不带则提示更新结果不完整。
2180
2428
  // 正常路径 prepared.personaText 已拼桥接段,这个分支只在异常数据时触发。
2181
- const staleBridge = detail.expertType === "team" && !hasTeamBridge(prepared.personaText);
2429
+ const staleBridge = detail.expertType === "team" && !expertApi.hasTeamBridge(prepared.personaText);
2182
2430
  return {
2183
2431
  presetId: result.presetId,
2184
2432
  presetMode: result.mode,
@@ -2200,9 +2448,10 @@ let McpManagerGateway = (() => {
2200
2448
  async wbExpertDismiss(payload) {
2201
2449
  const plugin = String(payload?.plugin || "").trim();
2202
2450
  if (!plugin) throw new Error("缺少 plugin");
2451
+ const expertApi = await loadExpert();
2203
2452
  const result = await withPatchWrite(this.ctx, async () => {
2204
2453
  const state = await managedState(this.ctx);
2205
- const content = removeExpertPreset(state.content, plugin);
2454
+ const content = expertApi.removeExpertPreset(state.content, plugin);
2206
2455
  if (content === null) return { mode: "absent" };
2207
2456
  await writePatch(state, content);
2208
2457
  return { mode: "removed" };
@@ -2210,14 +2459,14 @@ let McpManagerGateway = (() => {
2210
2459
  // 捆绑技能一并清掉(该 plugin 前缀下的全部目录;失败不回滚预设摘除——写是既成事实)。
2211
2460
  let removedSkills = [];
2212
2461
  try {
2213
- removedSkills = await uninstallExpertSkills(plugin);
2462
+ removedSkills = await expertApi.uninstallExpertSkills(plugin);
2214
2463
  } catch (error) {
2215
2464
  this.ctx.logger?.warn?.(`wb-expert: 预设已摘除,但技能清理失败:${error?.message ?? error}`);
2216
2465
  }
2217
2466
  // 团队型:成员人设目录同样按 plugin 隔离,一并清除。
2218
2467
  let removedTeam = null;
2219
2468
  try {
2220
- removedTeam = await uninstallTeamMemberPersonas(plugin);
2469
+ removedTeam = await expertApi.uninstallTeamMemberPersonas(plugin);
2221
2470
  } catch (error) {
2222
2471
  this.ctx.logger?.warn?.(`wb-expert: 预设已摘除,但成员人设清理失败:${error?.message ?? error}`);
2223
2472
  }
@@ -2238,7 +2487,8 @@ let McpManagerGateway = (() => {
2238
2487
  // 不存在的 TeamCreate/Agent(成员开不出来)。标记桥接判定放前端(需要市场目录的
2239
2488
  // expertType),这里只把 personaText 原样带出去。
2240
2489
  const patch = await readPatch(this.ctx);
2241
- const presets = readExpertPresets(patch.content);
2490
+ const expertApi = await loadExpert();
2491
+ const presets = expertApi.readExpertPresets(patch.content);
2242
2492
  // patch 只记 plugin / presetId,中英显示名在专家目录缓存里。这里补上两个字段,
2243
2493
  // 客户端按宿主语言挑:中文名给中文界面,英文名给英文界面。读缓存失败只回落
2244
2494
  // plugin 名,不影响「已召唤 / 已装技能」本身的状态判断。
@@ -2247,7 +2497,7 @@ let McpManagerGateway = (() => {
2247
2497
  // 所以目录没落盘 / 专家已下架时菜单仍能出「吴八哥」,而不是 senior-developer。
2248
2498
  let displayNames = new Map();
2249
2499
  try {
2250
- displayNames = await readExpertDisplayNames();
2500
+ displayNames = await expertApi.readExpertDisplayNames();
2251
2501
  } catch (error) {
2252
2502
  this.ctx.logger?.warn?.(`wb-expert: 读取专家显示名失败:${error?.message ?? error}`);
2253
2503
  }
@@ -2261,18 +2511,20 @@ let McpManagerGateway = (() => {
2261
2511
  nameEn: info.nameEn || row.displayName || "",
2262
2512
  };
2263
2513
  }),
2264
- skills: await listExpertSkills(),
2514
+ skills: await expertApi.listExpertSkills(),
2265
2515
  };
2266
2516
  }
2267
2517
  // 最近使用记录(输入框召唤菜单的数据源):召唤/送还时自动记录专家;技能与 MCP 由
2268
2518
  // 客户端在用户从菜单里显式选用时上报(recordUse)。只存展示所需最小字段。
2269
2519
  async wbExpertRecent() {
2270
- return listExpertRecent();
2520
+ const expertApi = await loadExpert();
2521
+ return expertApi.listExpertRecent();
2271
2522
  }
2272
2523
  async wbExpertRecordUse(payload) {
2273
2524
  const kind = String(payload?.kind || "").trim();
2274
2525
  const row = payload?.row && typeof payload.row === "object" ? payload.row : null;
2275
- return recordExpertRecent(kind, row);
2526
+ const expertApi = await loadExpert();
2527
+ return expertApi.recordExpertRecent(kind, row);
2276
2528
  }
2277
2529
  };
2278
2530
  })();