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